mirror of
https://github.com/msitarzewski/agency-agents.git
synced 2026-10-01 13:55:54 +03:00
Compare commits
97
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c078bd0db4 | ||
|
|
f16a99f47d | ||
|
|
6ab420d0b4 | ||
|
|
f0402f8c14 | ||
|
|
7edcb03234 | ||
|
|
c01335aa94 | ||
|
|
068cafcce5 | ||
|
|
8cc075bc83 | ||
|
|
90b09628e1 | ||
|
|
0051d7766d | ||
|
|
b36f739d3a | ||
|
|
f7dd810def | ||
|
|
2b9b2f1578 | ||
|
|
abc87a63ba | ||
|
|
f1e5d856ce | ||
|
|
8594e1dd73 | ||
|
|
f14ab6ff7f | ||
|
|
161de0779b | ||
|
|
db7b18e18f | ||
|
|
b929f1e521 | ||
|
|
e402360995 | ||
|
|
bbf1c7dec3 | ||
|
|
26678ec1c4 | ||
|
|
bfec2278e7 | ||
|
|
72c52affeb | ||
|
|
c1b4232c67 | ||
|
|
1462546714 | ||
|
|
b5c599dfdd | ||
|
|
385b3bb60a | ||
|
|
ca1ef694ea | ||
|
|
53ca655ad3 | ||
|
|
22148bee9c | ||
|
|
9daffc0202 | ||
|
|
f22a39c221 | ||
|
|
3023be035f | ||
|
|
83336b4c0a | ||
|
|
b94b72e780 | ||
|
|
f96c36ff3e | ||
|
|
83452ee559 | ||
|
|
057243b089 | ||
|
|
d85c598ce7 | ||
|
|
a518866195 | ||
|
|
5eacda1c7c | ||
|
|
ee241d4b70 | ||
|
|
ffdf7730bb | ||
|
|
947ba568e9 | ||
|
|
266c0864fe | ||
|
|
3711d8729a | ||
|
|
e9a1926f35 | ||
|
|
39bb5e4c96 | ||
|
|
b34a2af73b | ||
|
|
d9e8a51ea2 | ||
|
|
bdb3143fcd | ||
|
|
33f68d61b7 | ||
|
|
ec4ff9fed4 | ||
|
|
453bc88d47 | ||
|
|
3d80c07945 | ||
|
|
0a22d474b9 | ||
|
|
2692b54a67 | ||
|
|
751378c69b | ||
|
|
f68edd4583 | ||
|
|
43a77bb6e7 | ||
|
|
f3eeb37de5 | ||
|
|
d3185f2693 | ||
|
|
3fea7d499f | ||
|
|
a0f3137719 | ||
|
|
73322d5473 | ||
|
|
95ccaa639f | ||
|
|
ae127f185b | ||
|
|
ca830a9273 | ||
|
|
0e1374a84e | ||
|
|
d7d4c4b44b | ||
|
|
afc1f05209 | ||
|
|
1c5f946d31 | ||
|
|
5bc194a0a8 | ||
|
|
4edf283ce4 | ||
|
|
506bd2146b | ||
|
|
fc24e846a6 | ||
|
|
cf43138cb1 | ||
|
|
d0a2f2fe3d | ||
|
|
485e1be8e0 | ||
|
|
a36977f644 | ||
|
|
d52bacb488 | ||
|
|
b02fa83e4c | ||
|
|
5c729e483c | ||
|
|
5f51c10ae8 | ||
|
|
96917920f2 | ||
|
|
41ae439960 | ||
|
|
011a33280b | ||
|
|
06c1a8ddbd | ||
|
|
a3d1625728 | ||
|
|
cde130414c | ||
|
|
82425f3fe3 | ||
|
|
ae70562a83 | ||
|
|
c23759b7bc | ||
|
|
7b5df086f8 | ||
|
|
776cacf430 |
@@ -56,3 +56,12 @@ jobs:
|
||||
|
||||
- name: Reject unclosed agent frontmatter
|
||||
run: bash scripts/test-frontmatter-closing.sh
|
||||
|
||||
- name: Reject code fences that do not nest or never close
|
||||
run: bash scripts/test-lint-fences.sh
|
||||
|
||||
- name: Unknown options fail; --help lists every option
|
||||
run: bash scripts/test-cli-usage.sh
|
||||
|
||||
- name: Keep installing after one tool fails, then exit non-zero
|
||||
run: bash scripts/test-install-continue-after-failure.sh
|
||||
|
||||
@@ -104,3 +104,7 @@ jobs:
|
||||
run: |
|
||||
chmod +x scripts/lint-agents.sh
|
||||
./scripts/lint-agents.sh
|
||||
|
||||
- name: Reject folded frontmatter fixtures
|
||||
if: steps.rules.outputs.changed == 'true'
|
||||
run: bash scripts/test-folded-frontmatter.sh
|
||||
|
||||
@@ -29,3 +29,5 @@ jobs:
|
||||
chmod +x scripts/test-install.sh scripts/install.sh
|
||||
./scripts/test-install.sh
|
||||
bash scripts/test-install-openclaw-registration.sh
|
||||
bash scripts/test-install-link-ownership.sh
|
||||
bash scripts/test-install-parallel-logs.sh
|
||||
|
||||
@@ -102,6 +102,15 @@ Browse the agents below and copy/adapt the ones you need!
|
||||
./scripts/install.sh --tool opencode --division engineering --dry-run
|
||||
```
|
||||
|
||||
`--agent` and `--agents-file` take an agent's slug (as `--list agents` prints it), its display name, or its file name without `.md` — the id the [runbook rosters](strategy/runbooks.json) use — so a runbook's team installs as listed:
|
||||
|
||||
```bash
|
||||
python3 -c 'import json, sys
|
||||
for r in json.load(open("strategy/runbooks.json"))["runbooks"]:
|
||||
if r["slug"] == sys.argv[1]: [print(a) for g in r["roster"] for a in g["agents"]]' startup-mvp > team.txt
|
||||
./scripts/install.sh --tool claude-code --agents-file team.txt
|
||||
```
|
||||
|
||||
> **OpenCode note:** OpenCode's runtime currently registers only ~119 agents and silently drops the rest ([upstream bug](https://github.com/anomalyco/opencode/issues/27988)). Installing a subset with `--division` keeps you under that limit. The installer warns you when a selection would exceed it.
|
||||
|
||||
See the [Multi-Tool Integrations](#-multi-tool-integrations) section below for full details.
|
||||
|
||||
@@ -326,7 +326,7 @@ grep -i "target\|audience\|goal\|objective" ai/memory-bank/site-setup.md
|
||||
|
||||
## 📋 Your Deliverable Template
|
||||
|
||||
```markdown
|
||||
````markdown
|
||||
# [Project Name] Technical Architecture & UX Foundation
|
||||
|
||||
## 🏗️ CSS Architecture
|
||||
@@ -411,7 +411,7 @@ js/
|
||||
**Foundation Date**: [Date]
|
||||
**Developer Handoff**: Ready for LuxuryDeveloper implementation
|
||||
**Next Steps**: Implement foundation, then add premium polish
|
||||
```
|
||||
````
|
||||
|
||||
## 💭 Your Communication Style
|
||||
|
||||
|
||||
@@ -40,7 +40,11 @@ You are **API Platform Engineer**, an expert in building APIs that outside devel
|
||||
### Contract-First OpenAPI (the source of truth, reviewed before code)
|
||||
|
||||
```yaml
|
||||
# The spec is the contract. Consistency here is the whole product.
|
||||
# A complete minimal document, suitable for schema validation and SDK generation.
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Orders API
|
||||
version: 1.0.0
|
||||
paths:
|
||||
/v1/orders:
|
||||
post:
|
||||
@@ -52,10 +56,23 @@ paths:
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/OrderCreate' } } }
|
||||
responses:
|
||||
'201': { description: Created, content: { application/json: { schema: { $ref: '#/components/schemas/Order' } } } }
|
||||
'429': { description: Rate limited, headers: { Retry-After: { schema: { type: integer } } } }
|
||||
'429': { description: Rate limited, headers: { Retry-After: { description: Seconds until retry, schema: { type: integer, minimum: 0 } } } }
|
||||
default: { description: Error, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
|
||||
components:
|
||||
schemas:
|
||||
OrderCreate:
|
||||
type: object
|
||||
required: [product_id, quantity]
|
||||
properties:
|
||||
product_id: { type: string, format: uuid }
|
||||
quantity: { type: integer, minimum: 1 }
|
||||
Order:
|
||||
type: object
|
||||
required: [id, product_id, quantity]
|
||||
properties:
|
||||
id: { type: string, format: uuid }
|
||||
product_id: { type: string, format: uuid }
|
||||
quantity: { type: integer, minimum: 1 }
|
||||
Error: # ONE error shape, used everywhere — no exceptions
|
||||
type: object
|
||||
required: [code, message]
|
||||
@@ -63,9 +80,11 @@ components:
|
||||
code: { type: string, example: rate_limit_exceeded } # stable, machine-readable
|
||||
message: { type: string, example: "API rate limit exceeded; retry after 30s" }
|
||||
details: { type: object, description: "Field-level or contextual detail for self-diagnosis" }
|
||||
request_id:{ type: string, description: "Echo this to support — traceable on our side" }
|
||||
request_id: { type: string, description: "Echo this to support — traceable on our side" }
|
||||
```
|
||||
|
||||
Validate the whole document with an OpenAPI 3.1 validator before generating clients: YAML parsing alone cannot catch missing required document metadata or unresolved `$ref` targets. The minimal example defines every referenced schema; keep that invariant when extracting a larger contract. See the [OpenAPI 3.1 specification](https://spec.openapis.org/oas/v3.1.1.html).
|
||||
|
||||
### Backward-Compatibility Rules (memorize the two columns)
|
||||
|
||||
| Safe (additive — no version bump) | Breaking (needs new version + deprecation) |
|
||||
|
||||
@@ -264,7 +264,7 @@ When performing an ATS audit or designing an ATS validation engine, you must pro
|
||||
|
||||
### Deliverable 5: Agent-Native Export Prompt
|
||||
|
||||
```markdown
|
||||
````markdown
|
||||
## 🤖 Prompt Pronto para Agentes Externos (Claude / ChatGPT / Cursor)
|
||||
|
||||
```markdown
|
||||
@@ -286,7 +286,7 @@ REGRAS RÍGIDAS:
|
||||
3. Não exceda 30 palavras por bullet (evite sobrecarga cognitiva).
|
||||
4. Retorne apenas os bullets reescritos formatados em Markdown.
|
||||
```
|
||||
```
|
||||
````
|
||||
|
||||
## 🔄 Your Workflow Process
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ You are a **Data Engineer**, an expert in designing, building, and operating the
|
||||
|
||||
### Spark Pipeline (PySpark + Delta Lake)
|
||||
```python
|
||||
from datetime import date
|
||||
from pyspark.sql import SparkSession
|
||||
from pyspark.sql.functions import col, current_timestamp, sha2, concat_ws, lit
|
||||
from delta.tables import DeltaTable
|
||||
@@ -99,8 +100,18 @@ def upsert_silver(bronze_table: str, silver_table: str, pk_cols: list[str]) -> N
|
||||
source.write.format("delta").mode("overwrite").save(silver_table)
|
||||
|
||||
# ── Gold: aggregated business metric ─────────────────────────────────────────
|
||||
def build_gold_daily_revenue(silver_orders: str, gold_table: str) -> None:
|
||||
df = spark.read.format("delta").load(silver_orders)
|
||||
def build_gold_daily_revenue(
|
||||
silver_orders: str, gold_table: str, start_date: date, end_date: date
|
||||
) -> None:
|
||||
# Recompute an explicit half-open DATE window, including dates with no sales.
|
||||
# Deriving bounds from completed rows would leave stale revenue on an empty day.
|
||||
if start_date >= end_date:
|
||||
raise ValueError("start_date must be earlier than end_date")
|
||||
predicate = (
|
||||
f"order_date >= '{start_date.isoformat()}' "
|
||||
f"AND order_date < '{end_date.isoformat()}'"
|
||||
)
|
||||
df = spark.read.format("delta").load(silver_orders).filter(predicate)
|
||||
gold = df.filter(col("status") == "completed") \
|
||||
.groupBy("order_date", "region", "product_category") \
|
||||
.agg({"revenue": "sum", "order_id": "count"}) \
|
||||
@@ -108,10 +119,12 @@ def build_gold_daily_revenue(silver_orders: str, gold_table: str) -> None:
|
||||
.withColumnRenamed("count(order_id)", "order_count") \
|
||||
.withColumn("_refreshed_at", current_timestamp())
|
||||
gold.write.format("delta").mode("overwrite") \
|
||||
.option("replaceWhere", f"order_date >= '{gold['order_date'].min()}'") \
|
||||
.option("replaceWhere", predicate) \
|
||||
.save(gold_table)
|
||||
```
|
||||
|
||||
Run the Gold refresh against a complete Silver snapshot for the requested date window, not a partial event batch. Delta `replaceWhere` replaces exactly that window even when the aggregate is empty; dates outside it must remain untouched. Keep its default predicate constraint check enabled. For example, refreshing `[2026-09-01, 2026-09-02)` after an order is refunded must remove the old September 1 revenue, while preserving September 2 and later results. See [Delta selective overwrite](https://docs.delta.io/delta-batch/#selective-overwrite).
|
||||
|
||||
### dbt Data Quality Contract
|
||||
```yaml
|
||||
# models/silver/schema.yml
|
||||
|
||||
@@ -73,16 +73,23 @@ SELECT * FROM comments WHERE post_id = ?;
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
p.id, p.title, p.content,
|
||||
json_agg(json_build_object(
|
||||
'id', c.id,
|
||||
'content', c.content,
|
||||
'author', c.author
|
||||
)) as comments
|
||||
COALESCE(
|
||||
json_agg(json_build_object(
|
||||
'id', c.id,
|
||||
'content', c.content,
|
||||
'author', c.author
|
||||
) ORDER BY c.id) FILTER (WHERE c.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS comments
|
||||
FROM posts p
|
||||
LEFT JOIN comments c ON c.post_id = p.id
|
||||
WHERE p.user_id = 123
|
||||
GROUP BY p.id;
|
||||
|
||||
-- Regression cases: no comments => []; two comments => two ordered objects.
|
||||
-- FILTER removes the synthetic NULL row from LEFT JOIN; COALESCE turns an
|
||||
-- empty aggregate into the same array shape as a populated one.
|
||||
|
||||
-- Check the query plan:
|
||||
-- Look for: Seq Scan (bad), Index Scan (good), Bitmap Heap Scan (okay)
|
||||
-- Check: actual time vs planned time, rows vs estimated rows
|
||||
|
||||
@@ -19,17 +19,17 @@ You are **Database Reliability Engineer** (DBRE), an expert in keeping databases
|
||||
## 🎯 Your Core Mission
|
||||
- Design high availability: replication topology, automated failover, and quorum so a single node loss is a non-event, not an outage
|
||||
- Guarantee recoverability: automated backups, point-in-time recovery, and — the part everyone skips — regularly *tested* restores against real RPO/RTO targets
|
||||
- Make schema change safe: zero-downtime online migrations that never take a lock that stalls production, with an expand-contract discipline and a rollback plan
|
||||
- Make schema change safe: expand-contract migrations with measured lock budgets, bounded waits, batched backfills, and a rollback plan compatible with deployed writers
|
||||
- Protect the database from the application: connection pooling, sane limits, and backpressure so a client bug can't exhaust connections and topple the datastore
|
||||
- Rehearse disaster: scheduled failover and restore drills, documented runbooks, and DR that's been executed, not just diagrammed
|
||||
- **Default requirement**: Every backup strategy is validated by a real restore; every failover path is drilled; every schema migration is proven non-blocking before it touches production
|
||||
- **Default requirement**: Every backup strategy is validated by a real restore; every failover path is drilled; every schema migration has tested lock and statement budgets before it touches production
|
||||
|
||||
## 🚨 Critical Rules You Must Follow
|
||||
|
||||
1. **An untested backup is not a backup.** Backups that have never been restored are a hope, not a recovery plan. Automate restore verification on a schedule and measure the actual RTO — the first time you test a restore must never be during an incident.
|
||||
2. **Know your RPO and RTO, and prove you meet them.** How much data can you lose (RPO) and how long can you be down (RTO)? These are business decisions with technical consequences. Design backup frequency, replication, and failover to hit them, then verify with drills.
|
||||
3. **Failover must be drilled until it's boring.** An automated failover that's never been exercised will fail when it matters — promoting a lagging replica, splitting brain, or losing writes. Rehearse it on a schedule and fix what the drill exposes.
|
||||
4. **Never run a schema migration that takes a blocking lock in production.** A naive `ALTER`/`ADD COLUMN`/index build can lock a hot table and stall every query behind it. Use online/concurrent operations, expand-contract sequencing, and batched backfills — and verify the lock behavior before running it.
|
||||
4. **Budget every schema migration's locks.** Even metadata-only PostgreSQL `ADD COLUMN` takes an `ACCESS EXCLUSIVE` lock. Use a short `lock_timeout`, bounded statements, separate transactions, and a retry plan so waiting DDL cannot queue traffic indefinitely. Verify the engine's actual lock modes and keep scans/backfills out of exclusive-lock transactions.
|
||||
5. **Guard the connection layer.** Databases have hard connection limits; applications open connections faster than DBs can serve them. A pooler (PgBouncer / ProxySQL / equivalent) plus sane per-service limits is mandatory — connection exhaustion takes down a healthy database from the outside.
|
||||
6. **Replication lag is a correctness issue, not just a metric.** Reading from a lagging replica serves stale data; failing over to one loses writes. Monitor lag, gate read-after-write on it, and never promote a replica that's behind without understanding the data loss.
|
||||
7. **Every destructive or heavy operation needs a rollback and a blast-radius estimate.** Migrations, failovers, and large deletes get a written back-out plan and an impact assessment before execution — on a stateful system there is no `git revert`.
|
||||
@@ -77,27 +77,66 @@ Drill this on a schedule. A failover you haven't run is a failover you don't hav
|
||||
### Zero-Downtime Migration: Expand-Contract
|
||||
|
||||
```sql
|
||||
-- WRONG: locks the hot table, stalls production behind it
|
||||
-- ALTER TABLE orders ADD COLUMN status VARCHAR NOT NULL DEFAULT 'pending'; (blocking on many DBs)
|
||||
-- PostgreSQL example: short exclusive locks are still locks, not "non-blocking" DDL.
|
||||
-- On timeout, roll back the entire failed transaction and retry off peak.
|
||||
-- 1. EXPAND in its own short transaction; do not backfill while holding this lock.
|
||||
BEGIN;
|
||||
SET LOCAL lock_timeout = '1s';
|
||||
SET LOCAL statement_timeout = '5s';
|
||||
ALTER TABLE orders ADD COLUMN status VARCHAR;
|
||||
COMMIT;
|
||||
|
||||
-- RIGHT: expand-contract, no blocking lock, reversible at every step
|
||||
-- 1. EXPAND — add nullable column (fast, metadata-only), no default backfill lock
|
||||
ALTER TABLE orders ADD COLUMN status VARCHAR; -- instant, non-blocking
|
||||
-- 2. Set the default separately: new inserts that omit status receive 'pending'.
|
||||
-- Existing rows remain NULL, so unrelated UPDATEs can continue before backfill.
|
||||
BEGIN;
|
||||
SET LOCAL lock_timeout = '1s';
|
||||
SET LOCAL statement_timeout = '5s';
|
||||
ALTER TABLE orders ALTER COLUMN status SET DEFAULT 'pending';
|
||||
COMMIT;
|
||||
|
||||
-- 2. BACKFILL in batches so no single statement holds a long lock or bloats WAL
|
||||
UPDATE orders SET status = 'pending' WHERE status IS NULL AND id BETWEEN :lo AND :hi; -- loop
|
||||
-- 3. Deploy writers that never explicitly insert or update status to NULL;
|
||||
-- wait for ALL old writers to drain. Keep reads compatible with historical NULLs.
|
||||
-- 4. BACKFILL bounded batches, committing each batch (:lo/:hi are runner parameters).
|
||||
UPDATE orders SET status = 'pending'
|
||||
WHERE status IS NULL AND id BETWEEN :lo AND :hi;
|
||||
|
||||
-- 3. Dual-write from the app (new code writes status), deploy, let it bake
|
||||
-- 4. Add the constraint only after backfill is complete, validated separately:
|
||||
ALTER TABLE orders ADD CONSTRAINT status_not_null CHECK (status IS NOT NULL) NOT VALID;
|
||||
ALTER TABLE orders VALIDATE CONSTRAINT status_not_null; -- validates without a full-table lock
|
||||
-- 5. CONTRACT — remove old column/paths in a later release, once nothing reads them
|
||||
-- Every step is independently deployable and reversible. No maintenance window.
|
||||
-- 5. Gate new NULLs AFTER backfill: even a NOT VALID CHECK checks every UPDATE,
|
||||
-- including an unrelated column update on a legacy row whose status is NULL.
|
||||
BEGIN;
|
||||
SET LOCAL lock_timeout = '1s';
|
||||
SET LOCAL statement_timeout = '5s';
|
||||
ALTER TABLE orders ADD CONSTRAINT status_not_null
|
||||
CHECK (status IS NOT NULL) NOT VALID;
|
||||
COMMIT;
|
||||
|
||||
-- Indexes: always concurrently, so reads/writes continue during the build
|
||||
-- 6. VALIDATE separately: SHARE UPDATE EXCLUSIVE permits normal reads/writes,
|
||||
-- but can conflict with other maintenance/DDL. Set a realistic scan budget.
|
||||
BEGIN;
|
||||
SET LOCAL lock_timeout = '1s';
|
||||
SET LOCAL statement_timeout = '10min';
|
||||
ALTER TABLE orders VALIDATE CONSTRAINT status_not_null;
|
||||
COMMIT;
|
||||
|
||||
-- 7. Optional SET NOT NULL: on PostgreSQL 12+, a valid CHECK skips the table scan,
|
||||
-- but an ACCESS EXCLUSIVE lock is still needed. Drop the CHECK in a later step.
|
||||
BEGIN;
|
||||
SET LOCAL lock_timeout = '1s';
|
||||
SET LOCAL statement_timeout = '5s';
|
||||
ALTER TABLE orders ALTER COLUMN status SET NOT NULL;
|
||||
COMMIT;
|
||||
|
||||
-- CONTRACT old read paths only in a later release. After step 5, rolling back
|
||||
-- to a writer that explicitly writes NULL is unsafe until the constraint is relaxed.
|
||||
-- Index build is outside a transaction; concurrent builds still take locks.
|
||||
-- A failed concurrent build can leave an INVALID index: inspect it, then drop
|
||||
-- that invalid index before retrying (outside a transaction as well).
|
||||
CREATE INDEX CONCURRENTLY idx_orders_status ON orders (status);
|
||||
```
|
||||
|
||||
For a constant default such as this example's `'pending'`, PostgreSQL 11+ can instead add `status VARCHAR NOT NULL DEFAULT 'pending'` in one metadata-only operation, still under a short exclusive lock. The staged backfill pattern is needed when historical values must be computed per row; adapt the batch expression to that computation.
|
||||
|
||||
See [PostgreSQL ALTER TABLE lock and constraint semantics](https://www.postgresql.org/docs/current/sql-altertable.html). Test an open reader that forces step 1 to time out, an unrelated UPDATE on a legacy NULL row before its backfill, and an explicit NULL write after step 5. A failed batch can be replayed because it updates only NULL rows; validation is the proof that all historical rows now satisfy the invariant.
|
||||
|
||||
### Reliability Metrics & Guards
|
||||
|
||||
| Signal | Why it matters | Guard / alert |
|
||||
|
||||
@@ -73,7 +73,12 @@ ipcMain.handle('project:export', async (event, raw) => {
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
contextBridge.exposeInMainWorld('app', {
|
||||
exportProject: (req: unknown) => ipcRenderer.invoke('project:export', req),
|
||||
onUpdateReady: (cb: () => void) => ipcRenderer.on('update:ready', cb),
|
||||
onUpdateReady: (cb: () => void) => {
|
||||
// Electron's event object stays in preload; renderer callbacks receive no IPC internals.
|
||||
const listener = () => cb();
|
||||
ipcRenderer.on('update:ready', listener);
|
||||
return () => { ipcRenderer.removeListener('update:ready', listener); };
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -171,15 +171,23 @@ resource "aws_cloudwatch_metric_alarm" "high_cpu" {
|
||||
comparison_operator = "GreaterThanThreshold"
|
||||
evaluation_periods = "2"
|
||||
metric_name = "CPUUtilization"
|
||||
namespace = "AWS/ApplicationELB"
|
||||
period = "120"
|
||||
namespace = "AWS/EC2"
|
||||
dimensions = {
|
||||
AutoScalingGroupName = aws_autoscaling_group.app.name
|
||||
}
|
||||
# Matches EC2 basic monitoring's five-minute publication interval.
|
||||
period = "300"
|
||||
statistic = "Average"
|
||||
threshold = "80"
|
||||
|
||||
# Missing telemetry is unknown, not evidence that CPU is healthy.
|
||||
treat_missing_data = "missing"
|
||||
|
||||
alarm_actions = [aws_sns_topic.alerts.arn]
|
||||
}
|
||||
```
|
||||
|
||||
Before enabling an alarm, confirm its namespace, metric name, dimensions, and cadence against actual published datapoints. `CPUUtilization` belongs to [AWS/EC2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/viewing_metrics_with_cloudwatch.html), and `AutoScalingGroupName` selects this application's instances. `AWS/ApplicationELB` publishes load-balancer metrics, not instance CPU. Basic EC2 monitoring publishes every five minutes; use detailed monitoring explicitly if you need one-minute detection. Treat missing data as unknown and monitor telemetry loss separately.
|
||||
|
||||
### Monitoring and Alerting Configuration
|
||||
```yaml
|
||||
# Prometheus Configuration
|
||||
|
||||
@@ -128,7 +128,16 @@ def reconstruct_thread(messages):
|
||||
- Quoted replies duplicate content (20-msg thread = ~4-5x token bloat)
|
||||
- Thread forks when people reply to different messages in the chain
|
||||
"""
|
||||
# Build reply graph from In-Reply-To and References headers
|
||||
# Reject ambiguous identities before building or mutating the graph.
|
||||
# Missing/duplicate Message-ID must go to a quarantine/resolution path;
|
||||
# silently using None (or a reused ID) overwrites an unrelated message.
|
||||
messages = list(messages) # preserve support for one-pass message iterables
|
||||
message_ids = [msg.get("message_id") for msg in messages]
|
||||
if any(not isinstance(mid, str) or not mid.strip() for mid in message_ids):
|
||||
raise ValueError("Every message needs a nonempty Message-ID")
|
||||
if len(set(message_ids)) != len(message_ids):
|
||||
raise ValueError("Duplicate Message-ID: resolve identity before reconstruction")
|
||||
|
||||
graph = {}
|
||||
for msg in messages:
|
||||
parent_id = msg["in_reply_to"]
|
||||
|
||||
@@ -84,18 +84,29 @@ A commitment you don't fully utilize is a discount you paid for and threw away.
|
||||
### Unit Economics Dashboard (spend judged against value)
|
||||
|
||||
```sql
|
||||
-- Cost per active customer, trended — the number that tells growth from waste.
|
||||
-- Total cloud cost rising is fine IF cost-per-unit is flat or falling.
|
||||
SELECT
|
||||
date_trunc('month', usage_date) AS month,
|
||||
SUM(unblended_cost) AS total_cloud_cost,
|
||||
COUNT(DISTINCT customer_id) AS active_customers,
|
||||
SUM(unblended_cost) / NULLIF(COUNT(DISTINCT customer_id), 0) AS cost_per_customer,
|
||||
SUM(unblended_cost) FILTER (WHERE tag_environment = 'prod') AS prod_cost,
|
||||
SUM(unblended_cost) FILTER (WHERE tag_environment != 'prod') AS nonprod_cost
|
||||
FROM cost_and_usage
|
||||
JOIN customer_activity USING (usage_date)
|
||||
GROUP BY 1 ORDER BY 1;
|
||||
-- Aggregate each source to the reporting grain BEFORE joining.
|
||||
-- cost_and_usage: multiple line items/day; customer_activity: multiple events/day.
|
||||
WITH monthly_cost AS (
|
||||
SELECT date_trunc('month', usage_date) AS month,
|
||||
SUM(unblended_cost) AS total_cloud_cost,
|
||||
SUM(unblended_cost) FILTER (WHERE tag_environment = 'prod') AS prod_cost,
|
||||
SUM(unblended_cost) FILTER (WHERE tag_environment != 'prod') AS nonprod_cost
|
||||
FROM cost_and_usage
|
||||
GROUP BY 1
|
||||
), monthly_customers AS (
|
||||
SELECT date_trunc('month', usage_date) AS month,
|
||||
COUNT(DISTINCT customer_id) AS active_customers
|
||||
FROM customer_activity
|
||||
GROUP BY 1
|
||||
)
|
||||
SELECT c.month, c.total_cloud_cost,
|
||||
COALESCE(a.active_customers, 0) AS active_customers,
|
||||
c.total_cloud_cost / NULLIF(a.active_customers, 0) AS cost_per_customer,
|
||||
c.prod_cost, c.nonprod_cost
|
||||
FROM monthly_cost c
|
||||
LEFT JOIN monthly_customers a USING (month)
|
||||
ORDER BY c.month;
|
||||
-- Keep cost-only days/months; no observed active customers means unknown unit cost.
|
||||
-- Present alongside: allocated %, commitment coverage %, commitment utilization %.
|
||||
```
|
||||
|
||||
|
||||
@@ -56,16 +56,30 @@ develop ───●───●───●───●───●────
|
||||
### Starting Work
|
||||
```bash
|
||||
git fetch origin
|
||||
git checkout -b feat/my-feature origin/main
|
||||
# Or with worktrees for parallel work:
|
||||
git worktree add ../my-feature feat/my-feature
|
||||
git switch --no-track -c feat/my-feature origin/main
|
||||
# Publish your feature branch and set its upstream explicitly:
|
||||
git push -u origin feat/my-feature
|
||||
```
|
||||
|
||||
For parallel work, use this **instead of** creating the branch in the current
|
||||
checkout. Git cannot check out the same branch in two worktrees:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git worktree add --no-track -b feat/my-feature ../my-feature origin/main
|
||||
cd ../my-feature
|
||||
git push -u origin feat/my-feature
|
||||
```
|
||||
|
||||
`--no-track` prevents a new feature branch from inheriting `origin/main` as its
|
||||
upstream. After the first push, its upstream is `origin/feat/my-feature`.
|
||||
|
||||
### Clean Up Before PR
|
||||
```bash
|
||||
git fetch origin
|
||||
git rebase -i origin/main # squash fixups, reword messages
|
||||
git push --force-with-lease # safe force push to your branch
|
||||
# Only rewrite your own feature branch, with collaborators' agreement:
|
||||
git push --force-with-lease origin HEAD:feat/my-feature
|
||||
```
|
||||
|
||||
### Finishing a Branch
|
||||
|
||||
@@ -54,16 +54,31 @@ You are **Internationalization Engineer**, an expert in making software genuinel
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Rendering with FormatJS — the same message file drives web, and its format
|
||||
// (ICU) is what Android, iOS, and most TMS platforms speak natively.
|
||||
import { createIntl } from '@formatjs/intl';
|
||||
// Rendering with FormatJS: retain descriptions in the translator catalog,
|
||||
// but pass message strings (or compiled ASTs) to createIntl, not descriptors.
|
||||
import { createIntl, createIntlCache } from '@formatjs/intl';
|
||||
|
||||
const intl = createIntl({ locale: 'ar', messages: arMessages });
|
||||
const arMessages = {
|
||||
'cart.itemCount': {
|
||||
message: '{count, plural, =0 {سلتك فارغة} one {عنصر واحد} two {عنصران} few {# عناصر} many {# عنصرًا} other {# عنصر}}',
|
||||
description: 'Cart header; count = item count.',
|
||||
},
|
||||
};
|
||||
const messages = Object.fromEntries(
|
||||
Object.entries(arMessages).map(([id, descriptor]) => [id, descriptor.message])
|
||||
);
|
||||
const intl = createIntl({ locale: 'ar', messages }, createIntlCache());
|
||||
intl.formatMessage({ id: 'cart.itemCount' }, { count: 3 });
|
||||
// Arabic resolves count=3 to the CLDR "few" category — a form English doesn't have,
|
||||
// which is exactly why the ternary-operator version was a bug.
|
||||
```
|
||||
|
||||
Keep each target locale's ICU text and translator metadata separate at the
|
||||
runtime boundary. Passing `{message, description}` objects as `messages` entries
|
||||
causes formatting errors and can display the message ID instead of the translation.
|
||||
Mobile resource formats need a platform-specific export; an ICU catalog is not
|
||||
automatically an Android resource or an iOS String Catalog.
|
||||
|
||||
### Locale-Aware Formatting: Delete the Hand-Rolled Helpers
|
||||
|
||||
```javascript
|
||||
|
||||
@@ -132,14 +132,27 @@ challengeStore.put(user.id, options.challenge, { ttlSeconds: 300 });
|
||||
### Multi-Tenant Authorization: Isolation Below the Application
|
||||
|
||||
```sql
|
||||
-- Postgres row-level security: tenant scoping the ORM can't forget
|
||||
-- Use a restricted application role (no superuser or BYPASSRLS).
|
||||
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE documents FORCE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY tenant_isolation ON documents
|
||||
USING (tenant_id = current_setting('app.tenant_id')::uuid);
|
||||
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid)
|
||||
WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid);
|
||||
|
||||
-- Set from the AUTHENTICATED session at connection checkout — never from request input:
|
||||
-- SET app.tenant_id = '<tenant uuid from the verified session>';
|
||||
-- EVERY request runs all its queries on this same connection and transaction.
|
||||
-- Bind the authenticated tenant UUID using the driver's parameter API.
|
||||
BEGIN;
|
||||
SELECT set_config('app.tenant_id', CAST(:authenticated_tenant_id AS text), true);
|
||||
-- SELECT/INSERT/UPDATE/DELETE documents here, then COMMIT (or ROLLBACK on error).
|
||||
COMMIT;
|
||||
-- The true flag makes context transaction-local: pool reuse cannot carry a
|
||||
-- previous tenant into the next request. Missing context denies access.
|
||||
-- FORCE also subjects the table owner to RLS; privileged maintenance roles
|
||||
-- still bypass it and must never be used by request-serving connections.
|
||||
-- Objects called by requests must also use the caller's restricted privileges:
|
||||
-- avoid privileged SECURITY DEFINER functions and bypass-capable view owners;
|
||||
-- use security_invoker views where supported.
|
||||
```
|
||||
|
||||
## 🔄 Your Workflow Process
|
||||
|
||||
@@ -81,7 +81,7 @@ You are **Incident Response Commander**, an expert incident management specialis
|
||||
```
|
||||
|
||||
### Incident Response Runbook Template
|
||||
```markdown
|
||||
````markdown
|
||||
# Runbook: [Service/Failure Scenario Name]
|
||||
|
||||
## Quick Reference
|
||||
@@ -146,7 +146,7 @@ kubectl autoscale deployment/<service> -n production \
|
||||
- Internal: Post update in #incidents Slack channel
|
||||
- External: Update [status page link] if customer-facing
|
||||
- Follow-up: Create post-mortem document within 24 hours
|
||||
```
|
||||
````
|
||||
|
||||
### Post-Mortem Document Template
|
||||
```markdown
|
||||
|
||||
@@ -267,6 +267,7 @@ def build_ingest_graph(driver):
|
||||
g.add_node("merge", merge_node) # MERGE into Neo4j
|
||||
g.add_node("detect", detect_node) # contradiction Cypher
|
||||
g.add_node("verify", verify_node) # integrity gates
|
||||
g.set_entry_point("extract")
|
||||
g.add_edge("extract", "merge")
|
||||
g.add_edge("merge", "detect")
|
||||
g.add_edge("detect", "verify")
|
||||
|
||||
@@ -29,7 +29,7 @@ You are **Payments & Billing Engineer**, an expert in building payment integrati
|
||||
1. **Never touch raw card data.** Card numbers go from the customer's browser to the processor via hosted fields or SDK tokenization. If a PAN can reach your server, the design is wrong — that is the difference between SAQ A and a full PCI DSS audit.
|
||||
2. **Every mutation carries an idempotency key.** Charges, refunds, and subscription changes must be safely retryable. Derive the key from the business operation (order ID + attempt), not from a random UUID per HTTP call.
|
||||
3. **Webhooks are the source of truth, not the redirect.** Fulfill on `payment_intent.succeeded` (or the PSP equivalent), never on the customer returning to your success page. Customers close tabs; webhooks don't.
|
||||
4. **Verify signatures and deduplicate by event ID.** Reject unsigned or stale webhook payloads, persist processed event IDs, and make handlers safe to run twice.
|
||||
4. **Verify signatures and persist recoverable work.** Reject unsigned or stale webhook payloads; durably store verified events before acknowledgment. Deduplicate acceptance by event ID, mark completion only after successful processing, and make side effects safe to replay after a worker crash.
|
||||
5. **Store money as integers in minor units.** Amounts are `4999` cents with an ISO 4217 currency code — never floats, and never a bare number without its currency. Beware zero-decimal currencies like JPY.
|
||||
6. **Model every state, especially the unhappy ones.** `requires_action` (3DS), `processing`, partial refunds, disputes, and failed dunning retries are normal operating states, not edge cases to log-and-ignore.
|
||||
7. **Reconcile before you celebrate.** A green test suite proves the code path; only a payout-to-ledger reconciliation proves the money. Automate it daily and alert on any drift.
|
||||
@@ -60,42 +60,76 @@ export async function createPaymentForOrder(order: Order): Promise<Stripe.Paymen
|
||||
}
|
||||
```
|
||||
|
||||
### Webhook Handler: Signature, Dedupe, Out-of-Order Safety
|
||||
### Webhook Handler: Durable Acceptance Before Acknowledgment
|
||||
|
||||
Persist verified events in a durable inbox before returning `2xx`. An event ID alone is not a "processed" marker: if fulfillment crashes after inserting it, a retry must still find pending work. This example uses an application-owned inbox adapter with these explicit guarantees:
|
||||
|
||||
```typescript
|
||||
export async function handleStripeWebhook(req: Request): Promise<Response> {
|
||||
// 1. Verify the signature against the raw body — parsed JSON breaks verification
|
||||
const event = stripe.webhooks.constructEvent(
|
||||
await req.text(),
|
||||
req.headers.get('stripe-signature')!,
|
||||
process.env.STRIPE_WEBHOOK_SECRET!
|
||||
);
|
||||
interface WebhookInbox {
|
||||
// Atomic insert of ID + complete payload as pending, with a UNIQUE(event_id).
|
||||
// A duplicate never overwrites payload or resets completed work. Resolve only
|
||||
// after durable commit; reject on storage failure so the processor retries.
|
||||
accept(event: Stripe.Event): Promise<void>;
|
||||
// Atomically lease pending/expired work (e.g. FOR UPDATE SKIP LOCKED), increment
|
||||
// attempts, and return its payload. Reclaim expired leases after crashes;
|
||||
// move exhausted jobs to an inspectable dead-letter state instead of retrying forever.
|
||||
claim(maxAttempts: number): Promise<Stripe.Event | null>;
|
||||
// Mark complete only after side effects succeed. Pending/in-progress jobs
|
||||
// must remain retryable; the worker runner leases jobs and reclaims crashes.
|
||||
complete(eventId: string): Promise<void>;
|
||||
}
|
||||
|
||||
// 2. Deduplicate: at-least-once delivery means "twice" in practice
|
||||
const alreadyProcessed = await db.webhookEvents.insertIgnore({ id: event.id });
|
||||
if (alreadyProcessed) return new Response('duplicate', { status: 200 });
|
||||
export async function handleStripeWebhook(
|
||||
req: Request, inbox: WebhookInbox
|
||||
): Promise<Response> {
|
||||
const signature = req.headers.get('stripe-signature');
|
||||
if (!signature) return new Response('missing signature', { status: 400 });
|
||||
|
||||
// 3. Never trust event order — re-fetch current state instead of applying deltas
|
||||
let event: Stripe.Event;
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(
|
||||
await req.text(), signature, process.env.STRIPE_WEBHOOK_SECRET!
|
||||
);
|
||||
} catch {
|
||||
return new Response('invalid signature', { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
await inbox.accept(event); // includes duplicates whose original work is pending
|
||||
return new Response('accepted', { status: 200 });
|
||||
} catch {
|
||||
return new Response('storage unavailable', { status: 503 });
|
||||
}
|
||||
}
|
||||
|
||||
// The durable inbox worker invokes this with a leased pending event.
|
||||
// If it throws, retry with backoff; never mark the event complete in a finally block.
|
||||
export async function processStripeEvent(
|
||||
event: Stripe.Event, inbox: WebhookInbox
|
||||
): Promise<void> {
|
||||
switch (event.type) {
|
||||
case 'payment_intent.succeeded': {
|
||||
// Events can arrive out of order: re-fetch current processor state before acting.
|
||||
const pi = await stripe.paymentIntents.retrieve(
|
||||
(event.data.object as Stripe.PaymentIntent).id
|
||||
);
|
||||
if (pi.status === 'succeeded') {
|
||||
await fulfillOrder(pi.metadata.order_id); // must itself be idempotent
|
||||
await fulfillOrder(pi.metadata.order_id); // unique order fulfillment/outbox
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'charge.dispute.created':
|
||||
await freezeOrderAndNotifyFinance(event); // evidence deadline starts NOW
|
||||
await freezeOrderAndNotifyFinance(event); // dedupe notification by event.id
|
||||
break;
|
||||
}
|
||||
|
||||
// 4. Return 2xx fast; do heavy work in a queue so the PSP doesn't retry-storm you
|
||||
return new Response('ok', { status: 200 });
|
||||
await inbox.complete(event.id);
|
||||
}
|
||||
```
|
||||
|
||||
The worker scheduler, durable adapter, and domain handlers are application dependencies. `fulfillOrder` must atomically record fulfillment and any delivery outbox, or use downstream idempotency keys: a crash after the side effect but before `complete` replays the event. Distinct processor event IDs for the same order must also converge to one fulfillment. Signature failures return `400`; storage failures return `503`; acknowledged events remain recoverable without relying on processor redelivery.
|
||||
|
||||
Test four boundaries: failure before inbox commit, duplicate delivery while pending, worker failure before fulfillment, and worker crash after fulfillment but before completion. In each case the pending event must eventually complete with exactly one fulfillment. See [Stripe webhook delivery and signature guidance](https://docs.stripe.com/webhooks).
|
||||
|
||||
### Subscription Lifecycle State Machine
|
||||
|
||||
```text
|
||||
|
||||
@@ -333,104 +333,138 @@ export class PageGeometryEngine {
|
||||
Maintains a warm Chromium browser instance with pooled, isolated `BrowserContext` objects, concurrency rate limiting, route blocking for external noise, and scheduled recycling to deliver sub-80ms compilations:
|
||||
|
||||
```python
|
||||
# cv_pdf_pool.py: High-Throughput Browser Context Pool
|
||||
# cv_pdf_pool.py: Drain active jobs before recycling the shared browser.
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
from playwright.async_api import async_playwright, Browser, BrowserContext, Playwright
|
||||
from playwright.async_api import async_playwright, Browser, Playwright
|
||||
|
||||
logger = logging.getLogger("pdf_pool")
|
||||
|
||||
class PlaywrightPDFPool:
|
||||
def __init__(self, max_concurrency: int = 4, max_jobs_before_recycle: int = 500):
|
||||
self.max_concurrency = max_concurrency
|
||||
if max_concurrency < 1 or max_jobs_before_recycle < 1:
|
||||
raise ValueError("Pool limits must be positive")
|
||||
self.max_jobs_before_recycle = max_jobs_before_recycle
|
||||
self.semaphore = asyncio.Semaphore(max_concurrency)
|
||||
self.job_counter = 0
|
||||
self.playwright: Optional[Playwright] = None
|
||||
self.browser: Optional[Browser] = None
|
||||
self._lock = asyncio.Lock()
|
||||
self._condition = asyncio.Condition()
|
||||
self._active_jobs = 0
|
||||
self._recycling = False
|
||||
self._closed = False
|
||||
self._shutdown_task = None
|
||||
|
||||
async def initialize(self):
|
||||
async with self._lock:
|
||||
if self.browser and self.browser.is_connected():
|
||||
return
|
||||
self.playwright = await async_playwright().start()
|
||||
async def _close_locked(self):
|
||||
browser, playwright = self.browser, self.playwright
|
||||
self.browser = self.playwright = None
|
||||
try:
|
||||
if browser:
|
||||
await browser.close()
|
||||
finally:
|
||||
if playwright:
|
||||
await playwright.stop()
|
||||
|
||||
async def _initialize_locked(self):
|
||||
if self.browser and self.browser.is_connected():
|
||||
return
|
||||
if self._active_jobs:
|
||||
raise RuntimeError("Disconnected browser still has active jobs")
|
||||
await self._close_locked()
|
||||
self.playwright = await async_playwright().start()
|
||||
try:
|
||||
self.browser = await self.playwright.chromium.launch(
|
||||
headless=True,
|
||||
args=[
|
||||
"--disable-background-networking",
|
||||
"--disable-gpu",
|
||||
"--disable-dev-shm-usage",
|
||||
"--no-sandbox",
|
||||
"--font-render-hinting=none"
|
||||
]
|
||||
args=["--disable-background-networking", "--disable-gpu",
|
||||
"--disable-dev-shm-usage", "--no-sandbox", "--font-render-hinting=none"]
|
||||
)
|
||||
self.job_counter = 0
|
||||
logger.info("Playwright PDF Pool initialized with warm Chromium instance.")
|
||||
except BaseException:
|
||||
await self._close_locked()
|
||||
raise
|
||||
self.job_counter = 0
|
||||
|
||||
async def render_pdf(
|
||||
self,
|
||||
html_content: str,
|
||||
width_mm: float = 210.0,
|
||||
height_mm: float = 297.0
|
||||
) -> bytes:
|
||||
await self.initialize()
|
||||
async def initialize(self):
|
||||
async with self._condition:
|
||||
await self._condition.wait_for(lambda: not self._recycling or self._closed)
|
||||
if self._closed:
|
||||
raise RuntimeError("PDF pool is shut down")
|
||||
await self._initialize_locked()
|
||||
|
||||
async def render_pdf(self, html_content: str, width_mm: float = 210.0,
|
||||
height_mm: float = 297.0) -> bytes:
|
||||
async with self.semaphore:
|
||||
self.job_counter += 1
|
||||
if self.job_counter >= self.max_jobs_before_recycle:
|
||||
logger.info("Recycling browser process after %d jobs.", self.job_counter)
|
||||
await self.recycle()
|
||||
|
||||
# Create isolated context for the request
|
||||
context: BrowserContext = await self.browser.new_context(
|
||||
viewport={"width": int(width_mm * 96 / 25.4), "height": int(height_mm * 96 / 25.4)},
|
||||
device_scale_factor=1.0
|
||||
)
|
||||
async with self._condition:
|
||||
# At the threshold, stop admitting new jobs until this
|
||||
# generation drains; sustained traffic cannot starve recycling.
|
||||
await self._condition.wait_for(lambda: self._closed or (
|
||||
not self._recycling and (self.job_counter < self.max_jobs_before_recycle
|
||||
or self._active_jobs == 0)
|
||||
))
|
||||
if self._closed:
|
||||
raise RuntimeError("PDF pool is shut down")
|
||||
# Drain at the configured threshold without interrupting active
|
||||
# renders or reacquiring a lock held by the same coroutine.
|
||||
if self._active_jobs == 0 and self.job_counter >= self.max_jobs_before_recycle:
|
||||
await self._close_locked()
|
||||
await self._initialize_locked()
|
||||
browser = self.browser
|
||||
self._active_jobs += 1
|
||||
self.job_counter += 1
|
||||
|
||||
context = None
|
||||
try:
|
||||
page = await context.new_page()
|
||||
|
||||
# Abort tracking and off-target external requests
|
||||
await page.route(
|
||||
"**/*",
|
||||
lambda route: route.abort() if route.request.resource_type in ["media", "websocket"] else route.continue_()
|
||||
context = await browser.new_context(
|
||||
viewport={"width": int(width_mm * 96 / 25.4), "height": int(height_mm * 96 / 25.4)},
|
||||
device_scale_factor=1.0
|
||||
)
|
||||
|
||||
# Load HTML with networkidle guarantee
|
||||
page = await context.new_page()
|
||||
await page.route("**/*", lambda route: route.abort()
|
||||
if route.request.resource_type in ["media", "websocket"]
|
||||
else route.continue_())
|
||||
await page.set_content(html_content, wait_until="networkidle")
|
||||
await page.evaluate("document.fonts.ready")
|
||||
|
||||
# Generate tagged, vector-clean PDF via CDP
|
||||
pdf_bytes = await page.pdf(
|
||||
width=f"{width_mm}mm",
|
||||
height=f"{height_mm}mm",
|
||||
print_background=True,
|
||||
prefer_css_page_size=True,
|
||||
tagged=True,
|
||||
return await page.pdf(
|
||||
width=f"{width_mm}mm", height=f"{height_mm}mm",
|
||||
print_background=True, prefer_css_page_size=True, tagged=True,
|
||||
margin={"top": "0mm", "right": "0mm", "bottom": "0mm", "left": "0mm"}
|
||||
)
|
||||
return pdf_bytes
|
||||
finally:
|
||||
await context.close()
|
||||
try:
|
||||
if context:
|
||||
await context.close()
|
||||
finally:
|
||||
async with self._condition:
|
||||
self._active_jobs -= 1
|
||||
self._condition.notify_all()
|
||||
|
||||
async def recycle(self):
|
||||
async with self._lock:
|
||||
if self.browser:
|
||||
await self.browser.close()
|
||||
if self.playwright:
|
||||
await self.playwright.stop()
|
||||
self.browser = None
|
||||
self.playwright = None
|
||||
await self.initialize()
|
||||
async with self._condition:
|
||||
await self._condition.wait_for(lambda: not self._recycling or self._closed)
|
||||
if self._closed:
|
||||
raise RuntimeError("PDF pool is shut down")
|
||||
self._recycling = True
|
||||
try:
|
||||
await self._condition.wait_for(lambda: self._active_jobs == 0)
|
||||
await self._close_locked()
|
||||
await self._initialize_locked()
|
||||
finally:
|
||||
self._recycling = False
|
||||
self._condition.notify_all()
|
||||
|
||||
async def _finish_shutdown(self):
|
||||
async with self._condition:
|
||||
self._condition.notify_all()
|
||||
await self._condition.wait_for(lambda: self._active_jobs == 0 and not self._recycling)
|
||||
await self._close_locked()
|
||||
|
||||
async def shutdown(self):
|
||||
async with self._lock:
|
||||
if self.browser:
|
||||
await self.browser.close()
|
||||
if self.playwright:
|
||||
await self.playwright.stop()
|
||||
# Retain cleanup: cancelling a caller must not abandon the browser
|
||||
# after in-flight renders finish.
|
||||
self._closed = True
|
||||
if self._shutdown_task is None:
|
||||
self._shutdown_task = asyncio.create_task(self._finish_shutdown())
|
||||
await asyncio.shield(self._shutdown_task)
|
||||
```
|
||||
|
||||
### 4. 1:1 Sheet Canvas Viewport Scaler Architecture (CSS & React)
|
||||
|
||||
@@ -70,7 +70,7 @@ You've built these systems for real workloads: multilingual corpora, domain-spec
|
||||
### Chunking Strategy — Semantic + Structural
|
||||
|
||||
```python
|
||||
from langchain.text_splitter import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter
|
||||
from langchain_text_splitters import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter
|
||||
|
||||
def chunk_document(text: str, doc_type: str) -> list[dict]:
|
||||
"""
|
||||
@@ -96,7 +96,7 @@ def chunk_document(text: str, doc_type: str) -> list[dict]:
|
||||
for doc in header_chunks:
|
||||
sub_chunks = char_splitter.split_documents([doc])
|
||||
chunks.extend(sub_chunks)
|
||||
return chunks
|
||||
return [{"content": doc.page_content, "metadata": doc.metadata} for doc in chunks]
|
||||
|
||||
else:
|
||||
# Semantic chunking for unstructured text
|
||||
@@ -105,7 +105,10 @@ def chunk_document(text: str, doc_type: str) -> list[dict]:
|
||||
chunk_overlap=80,
|
||||
separators=["\n\n", "\n", ". ", "! ", "? ", " "]
|
||||
)
|
||||
return splitter.create_documents([text])
|
||||
return [
|
||||
{"content": doc.page_content, "metadata": doc.metadata}
|
||||
for doc in splitter.create_documents([text])
|
||||
]
|
||||
```
|
||||
|
||||
### pgvector Schema & HNSW Index
|
||||
@@ -141,6 +144,7 @@ CREATE INDEX ON document_chunks (document_id);
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import json
|
||||
from openai import AsyncOpenAI
|
||||
from pgvector.asyncpg import register_vector
|
||||
import asyncpg
|
||||
@@ -177,7 +181,7 @@ async def ingest_document(document_id: str, chunks: list[dict], pool: asyncpg.Po
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
""",
|
||||
[
|
||||
(document_id, c["content"], emb, idx, c.get("metadata", {}))
|
||||
(document_id, c["content"], emb, idx, json.dumps(c.get("metadata", {})))
|
||||
for idx, (c, emb) in enumerate(zip(chunks, embeddings))
|
||||
]
|
||||
)
|
||||
|
||||
@@ -41,7 +41,8 @@ You are **Realtime Collaboration Engineer**, an expert in the systems behind liv
|
||||
|
||||
```typescript
|
||||
// The contract: server assigns seq to every op; client acks what it has applied;
|
||||
// resume replays the gap. Duplicates are impossible by construction (opId dedupe).
|
||||
// resume replays the gap. Server opId dedupe prevents duplicate log entries;
|
||||
// clients must separately ignore replayed deliveries and reject sequence gaps.
|
||||
class SyncConnection {
|
||||
private lastServerSeq = 0; // highest seq applied locally
|
||||
private pending = new Map<string, Op>(); // sent, not yet acked
|
||||
@@ -66,9 +67,16 @@ class SyncConnection {
|
||||
|
||||
private receive(msg: ServerMsg) {
|
||||
if (msg.type === 'op') {
|
||||
this.lastServerSeq = msg.seq; // server ordering is truth
|
||||
this.pending.delete(msg.opId); // ack of our own op, or...
|
||||
this.applyRemote(msg); // ...someone else's, transformed
|
||||
if (msg.seq <= this.lastServerSeq) return; // replay: already applied
|
||||
if (msg.seq !== this.lastServerSeq + 1) {
|
||||
// Keep the contiguous cursor: reconnect/replay from the last applied op.
|
||||
// Closing triggers the existing onclose reconnect path.
|
||||
this.ws.close();
|
||||
return;
|
||||
}
|
||||
this.applyRemote(msg); // may throw; do not advance yet
|
||||
this.lastServerSeq = msg.seq;
|
||||
this.pending.delete(msg.opId); // ack only after successful apply
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ You are a **Technical Writer**, a documentation specialist who bridges the gap b
|
||||
## 📋 Your Technical Deliverables
|
||||
|
||||
### High-Quality README Template
|
||||
```markdown
|
||||
````markdown
|
||||
# Project Name
|
||||
|
||||
> One-sentence description of what this does and why it matters.
|
||||
@@ -120,7 +120,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
## License
|
||||
|
||||
MIT © [Your Name](https://github.com/yourname)
|
||||
```
|
||||
````
|
||||
|
||||
### OpenAPI Documentation Example
|
||||
```yaml
|
||||
@@ -203,7 +203,7 @@ paths:
|
||||
```
|
||||
|
||||
### Tutorial Structure Template
|
||||
```markdown
|
||||
````markdown
|
||||
# Tutorial: [What They'll Build] in [Time Estimate]
|
||||
|
||||
**What you'll build**: A brief description of the end result with a screenshot or demo link.
|
||||
@@ -255,7 +255,7 @@ You built a [description]. Here's what you learned:
|
||||
- [Advanced tutorial: Add authentication](link)
|
||||
- [Reference: Full API docs](link)
|
||||
- [Example: Production-ready version](link)
|
||||
```
|
||||
````
|
||||
|
||||
### Docusaurus Configuration
|
||||
```javascript
|
||||
|
||||
@@ -254,14 +254,32 @@ export function executeReorderTransaction(
|
||||
): { updatedYaml: string; changedRange: [number, number] } {
|
||||
const doc = parseDocument(yamlSource, { keepSourceTokens: true });
|
||||
|
||||
const seqPath = intent.targetSequencePointer.split('/').filter(Boolean);
|
||||
const targetSeq = doc.getIn(seqPath);
|
||||
if (doc.errors.length) throw new Error('Cannot reorder invalid YAML.');
|
||||
const decodePointer = (pointer: string): string[] => {
|
||||
if (pointer === '') return [];
|
||||
if (!pointer.startsWith('/') || /~(?![01])/.test(pointer)) {
|
||||
throw new Error('Invalid JSON pointer.');
|
||||
}
|
||||
return pointer.slice(1).split('/').map(part => part.replace(/~1/g, '/').replace(/~0/g, '~'));
|
||||
};
|
||||
const seqPath = decodePointer(intent.targetSequencePointer);
|
||||
const sourcePath = decodePointer(intent.sourcePointer);
|
||||
const indexToken = sourcePath.pop();
|
||||
if (JSON.stringify(sourcePath) !== JSON.stringify(seqPath) || !/^(0|[1-9]\d*)$/.test(indexToken ?? '')) {
|
||||
throw new Error('Source must be an item in the target sequence.');
|
||||
}
|
||||
const targetSeq = seqPath.length ? doc.getIn(seqPath) : doc.contents;
|
||||
|
||||
if (!isSeq(targetSeq)) {
|
||||
throw new Error(`Target at pointer ${intent.targetSequencePointer} is not a valid sequence.`);
|
||||
}
|
||||
|
||||
const sourceIndex = parseInt(intent.sourcePointer.split('/').pop() || '0', 10);
|
||||
const sourceIndex = Number(indexToken);
|
||||
if (!Number.isSafeInteger(sourceIndex) || sourceIndex >= targetSeq.items.length ||
|
||||
!Number.isInteger(intent.targetIndex) || intent.targetIndex < 0 ||
|
||||
intent.targetIndex >= targetSeq.items.length) {
|
||||
throw new Error('Reorder indices must identify valid final sequence positions.');
|
||||
}
|
||||
const [movedNode] = targetSeq.items.splice(sourceIndex, 1);
|
||||
targetSeq.items.splice(intent.targetIndex, 0, movedNode);
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ You are **Video Streaming Engineer**, an expert in delivering video that plays i
|
||||
|
||||
```bash
|
||||
# Encode a multi-rung ladder with aligned keyframes (GOP) so ABR can switch
|
||||
# cleanly at segment boundaries. Keyframe interval = segment duration * fps.
|
||||
# cleanly at segment boundaries. Force 24fps before the 48-frame (2s) closed GOP.
|
||||
# Keep comments on separate lines: a continuation backslash must end its line.
|
||||
ffmpeg -i source.mov \
|
||||
-filter_complex "[0:v]split=4[v1][v2][v3][v4]; \
|
||||
[v1]scale=w=640:h=360[v360]; [v2]scale=w=1280:h=720[v720]; \
|
||||
@@ -50,7 +51,7 @@ ffmpeg -i source.mov \
|
||||
-map "[v720]" -c:v:1 libx264 -b:v:1 2800k -maxrate:1 2996k -bufsize:1 4200k \
|
||||
-map "[v1080]" -c:v:2 libx264 -b:v:2 5000k -maxrate:2 5350k -bufsize:2 7500k \
|
||||
-map "[v1440]" -c:v:3 libx264 -b:v:3 8000k -maxrate:3 8560k -bufsize:3 12000k \
|
||||
-x264-params "keyint=48:min-keyint=48:scenecut=0" \ # closed GOP, 2s @ 24fps, aligned across rungs
|
||||
-r 24 -flags +cgop -x264-params "keyint=48:min-keyint=48:scenecut=0" \
|
||||
-map a:0 -c:a aac -b:a 128k \
|
||||
-f null - # (real pipeline pipes to a CMAF packager; keyframe alignment is the point here)
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ def preprocess_audio(input_path: str, output_path: str) -> str:
|
||||
|
||||
|
||||
def chunk_audio(input_path: str, chunk_dir: str,
|
||||
chunk_duration: int = 1800, overlap: int = 30) -> list[str]:
|
||||
chunk_duration: int = 1800, overlap: int = 30) -> list[dict]:
|
||||
"""
|
||||
Split long audio into overlapping chunks for model processing.
|
||||
|
||||
@@ -189,11 +189,17 @@ def chunk_audio(input_path: str, chunk_dir: str,
|
||||
overlap: overlap window in seconds (default 30s)
|
||||
"""
|
||||
import math, os
|
||||
if not math.isfinite(chunk_duration) or chunk_duration <= 0:
|
||||
raise ValueError("chunk_duration must be finite and positive")
|
||||
if not math.isfinite(overlap) or overlap < 0:
|
||||
raise ValueError("overlap must be finite and nonnegative")
|
||||
result = subprocess.run([
|
||||
"ffprobe", "-v", "quiet", "-show_entries", "format=duration",
|
||||
"-of", "default=noprint_wrappers=1:nokey=1", input_path
|
||||
], capture_output=True, text=True, check=True)
|
||||
total_duration = float(result.stdout.strip())
|
||||
if not math.isfinite(total_duration) or total_duration <= 0:
|
||||
raise ValueError("Audio duration must be finite and positive")
|
||||
|
||||
chunks = []
|
||||
start = 0
|
||||
@@ -205,10 +211,11 @@ def chunk_audio(input_path: str, chunk_dir: str,
|
||||
out_path = f"{chunk_dir}/chunk_{chunk_index:04d}.wav"
|
||||
subprocess.run([
|
||||
"ffmpeg", "-y",
|
||||
"-i", input_path,
|
||||
"-ss", str(start),
|
||||
"-to", str(end),
|
||||
"-acodec", "copy",
|
||||
"-i", input_path,
|
||||
"-t", str(end - start),
|
||||
"-map", "0:a:0", "-vn",
|
||||
"-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1",
|
||||
out_path
|
||||
], check=True, capture_output=True)
|
||||
chunks.append({"path": out_path, "start_offset": start, "index": chunk_index})
|
||||
|
||||
@@ -106,7 +106,12 @@ const request = (options) => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(res.data);
|
||||
} else {
|
||||
reject({ code: res.statusCode, message: res.data.message || 'Request failed' });
|
||||
// Error bodies may be empty, plain text, or a gateway HTML page.
|
||||
// Do not throw inside the success callback and leave the Promise pending.
|
||||
const message = res.data && typeof res.data === 'object' &&
|
||||
typeof res.data.message === 'string' && res.data.message
|
||||
? res.data.message : 'Request failed';
|
||||
reject({ code: res.statusCode, message });
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
|
||||
@@ -64,6 +64,9 @@ class PIPELINE_OT_validate_assets(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
issues = []
|
||||
if not context.selected_objects:
|
||||
self.report({'WARNING'}, "Select assets before validation/export.")
|
||||
return {'CANCELLED'}
|
||||
for obj in context.selected_objects:
|
||||
if obj.type != "MESH":
|
||||
continue
|
||||
@@ -74,8 +77,8 @@ class PIPELINE_OT_validate_assets(bpy.types.Operator):
|
||||
if any(abs(s - 1.0) > 0.0001 for s in obj.scale):
|
||||
issues.append(f"{obj.name}: unapplied scale")
|
||||
|
||||
if len(obj.material_slots) == 0:
|
||||
issues.append(f"{obj.name}: missing material slot")
|
||||
if not obj.material_slots or any(slot.material is None for slot in obj.material_slots):
|
||||
issues.append(f"{obj.name}: missing assigned material")
|
||||
|
||||
if issues:
|
||||
self.report({'WARNING'}, f"Validation found {len(issues)} issue(s). See system console.")
|
||||
@@ -111,6 +114,10 @@ class PIPELINE_OT_export_selected(bpy.types.Operator):
|
||||
bl_label = "Export Selected"
|
||||
|
||||
def execute(self, context):
|
||||
# The export button must enforce the same gate as the validation button.
|
||||
if bpy.ops.pipeline.validate_assets() != {'FINISHED'}:
|
||||
self.report({'WARNING'}, "Export blocked: resolve asset validation findings.")
|
||||
return {'CANCELLED'}
|
||||
export_path = context.scene.pipeline_export_path
|
||||
bpy.ops.export_scene.gltf(
|
||||
filepath=export_path,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# convert-outputs manifest v2 — one line per agent (its output across every tool), one per tool
|
||||
# (non-agent files), one per contract. Platform-neutral hashes. Regenerate: scripts/test-convert-outputs.sh --update
|
||||
agent 3d-scene-developer cb46371b7ee6fa5fabfc5da1094a2b1a552e4adc1a76fcd44b604c2e0c9fed27
|
||||
agent accessibility-auditor 9e873dda6ef18f2e2f058008fb051f09b9b95d1c9285f1bbccb5d94e54ece890
|
||||
agent accessibility-auditor f797a3000f39d0c91a1722c5e4004a6db7f1d665fd069ea9e8c16be32c9c8a13
|
||||
agent account-strategist 474bd07c750f87c202f214dab515ab7fb9c82394c62b9d5752902d5eb7fbd8a2
|
||||
agent accounts-payable-agent 5079310c10346460332645bdcaea34a303a8ee9e9092cbba930c1c9602bda3d3
|
||||
agent ad-creative-strategist e288208090b93e5d5d9cb63412788b14f0c9aee796f8513458abb5496ff4b27f
|
||||
@@ -16,11 +16,11 @@ agent ai-engineer d93416c98c7462beb379753a9416ed94e8c4b8f5f680a3f1b7fb5904e60f70
|
||||
agent ai-generated-code-security-auditor 0e706372c85d1c491e33bd505dccdb5cdbb86ec244e3a5d0c3a4c89afe77976b
|
||||
agent analytics-reporter 84bbf7a058e826b0f416556403353fea224432184a1bab1f230eb1b556387301
|
||||
agent anthropologist c79922e2739ea11715729b4535e2b32f6c0d861cd1f12915bdfb4584eff05981
|
||||
agent api-platform-engineer 339d88f0c427b19fc24b66024a3f6c7697ecbd768935731c2a167400662f4b69
|
||||
agent api-tester 94352c1e8dae8b0ef010d1a11748bd83fb83806b681c415409b9f18b8f167412
|
||||
agent api-platform-engineer 952d2e1552d5629fad4c5edef70d3531a4686daf3728450afeb9ac935eaaa3aa
|
||||
agent api-tester fc5016240261651d68832db41c41fb218b6b91f2db6ad1d5a45ace361a1b0bef
|
||||
agent app-store-optimizer 4b4f5e0cec69b402b824970817b23774b00f46e7ce0b3d2e3ebd3278cdfea002
|
||||
agent application-security-engineer 4f40e4a8354f1bf4880ee94db59a08adae29b81ba941edc07f5820b752ebfaa3
|
||||
agent ats-validator-architect 8d330b7ef5405660883e9eef2da00cae52b144077fa65ea9ac17c7c8112f6f13
|
||||
agent application-security-engineer d12e4555fcacb2d12ea386c21708430ca0853986646c9b2939dd6c08c32ef264
|
||||
agent ats-validator-architect d79845cb837f233257ea51e6542f6b86e67ea238cc25014d0eddaf5cd8accae6
|
||||
agent automation-governance-architect 181383ad60f2f885b034b1dd3560e77c906eaf102540e601c5bd23008fe6d83c
|
||||
agent autonomous-optimization-architect de8397b19bc17516e5ae41a9c55df9894ccfffe4eeb70382930875a60dbaa369
|
||||
agent backend-architect fdabc3c9625b5852594169f017fd1f8e100af9bc5c2b2eb3728256f8da803295
|
||||
@@ -28,8 +28,8 @@ agent baidu-seo-specialist 19aecfcdb5a828592a56c602182cd483d3445e684e85bbedefa62
|
||||
agent behavioral-nudge-engine 64be8cb90de0fb15386ac43321141ddf572b9d034af4ffc3faf955947064ba09
|
||||
agent bilibili-content-strategist c0bdf892f215a743b716713b0f037c042ffefa3acafffeb4111403a9d20d1b3f
|
||||
agent bim-gis-specialist ea191ab15ab0b25dd3705e83842bf59b3e7019b143e54ac0a250538559de46de
|
||||
agent blender-add-on-engineer 0bef2e8a241622429e07e9cdbf0ffe47ba3f00eea22bea33d340cb586a07343e
|
||||
agent blockchain-security-auditor 8fa9dcb81ed6a9272fee39222e1d4923370a3ff56efea20bf0cb3ba6a7d2c085
|
||||
agent blender-add-on-engineer 74e5fbb003867f27a0a17e24e3cfda6e1cd546d3e04e2310d521598e1a5de8d7
|
||||
agent blockchain-security-auditor e60938bb97086821d2bd0a8cd42364a602755126f3d22e32b23aef29a25584be
|
||||
agent book-co-author 459ff00b518d9f8f77ee765cb1b4e5f0b427ae733f1714a527350cbf99d7bedc
|
||||
agent bookkeeper-controller 73acf3a92e347510f0e76089e2e9bb9899d4a6f1890648423ce075affb0909d6
|
||||
agent brand-guardian b095aeda9a0de234c166ae18a2ff3535501d755eb77f83757583ba5c42a44dc5
|
||||
@@ -44,7 +44,7 @@ agent china-market-localization-strategist 4e144930de0c803ae930981d6e3577f4fa035
|
||||
agent china-network-engineer 406a6032cc7292f28fa46e69ed43bc7e35ec987d9ef8477377e92982cb0b95b3
|
||||
agent civil-engineer e0e0505c9ccc978a7a18399d4e3cc37ee5961255d76e393c538d94d896f5fc2c
|
||||
agent clinical-evidence-agent 1e33191bdad4e58e0c61476fdbf47c92833705df8809e138b713f16dd3711f0d
|
||||
agent cloud-security-architect af68f7640a57e6f277e9453bb66ef5fc63a4b018e820b8146502f4e1b3e9bc88
|
||||
agent cloud-security-architect c3723f4f18fdd5ae947dfe70a565965c48d364d937568013ee98f5c7f24abb6b
|
||||
agent cms-developer 0884de84176b0f78f8a704b86a1a9effaf0bc8459bf771a08c0aa8e1f741e1d8
|
||||
agent code-reviewer 43a29ff0f7e96405412d84c8a655f1f9947bdd2d26182ed579cedd8d2deb8d3d
|
||||
agent codebase-archaeologist 56ffbf032397192d0b2c047a32c729b79214e056468f2de8812f4d598d6967cf
|
||||
@@ -57,17 +57,17 @@ agent cultural-intelligence-strategist a2fcfd37a164bc8aef96eb048f527498392322222
|
||||
agent customer-service 5f4798b742303ee10cc8b387845a05bfc649ff7787b2974e197ba2651354b1d2
|
||||
agent customer-success-manager 2b07e9e2632ccdb0e49d9340bb1d1d54ceceefbd50a35f4361c8fb452894bcfc
|
||||
agent data-consolidation-agent 1dd9d3c4c264ddee3fe2e4e7e86fe2473c99df505d83b48eb6e0daa75d8a300a
|
||||
agent data-engineer 650387aec75a738801413d7b4d0d9347892072a5eda5cef1469c79338e8327dc
|
||||
agent data-engineer 71403e6c6e8a4eeb8ed85f6e2a5c1e86a9bb92f5073d61ab5a821925c91c50c4
|
||||
agent data-privacy-officer 6795a5dc408218f49614942bb0bbf2022d673eddd85251d6446083c4c8e84235
|
||||
agent data-visualization-engineer 869c6d2bb3699de027b6baa2c7c5d8790515bb9bcb54188726d0ae1222dd2a42
|
||||
agent database-optimizer 20c71a07f482837cc5e7b111563719babdf7248163347ec3bae8684089338119
|
||||
agent database-reliability-engineer e1ef00f76f15c9b7cdee96587d4f09d8037514cd5ccf8fce5187cb157ed5c658
|
||||
agent database-optimizer 6a83d16d4482fcc84c4517aae827ca0823a02c06c082ccb87fe2631eb60ce43a
|
||||
agent database-reliability-engineer 1a57963c5bea5e2f9f9f51e67af536072915d6e7140cc10a308a6344156e01eb
|
||||
agent deal-strategist 491bda620283290a8d2af0edaafc0b773128bcb319daafaeb0887049268a65d3
|
||||
agent desktop-app-engineer 8346e9b37b3989dd9d3f1fc91b7093580476880919a881cef237012f1b5c7b2a
|
||||
agent developer-advocate 59a813fc4c7b022e56a41f2f666140eea4fd71e286ff8a86bcc54d88ea719ca6
|
||||
agent desktop-app-engineer c8769b70f4411efc8e7ea1898908fc9f92bd673746b363c2accddf554b5dcbfc
|
||||
agent developer-advocate bae8b3a650efe5225d7d75affa59147af809e27d85244e56e7e5906fad160a80
|
||||
agent developer-community-builder e523c237b0fe0380c3912b6b86bc722d8d0ab2ec622b22c3f3e22ff3a99ed97e
|
||||
agent developer-tooling-engineer 8e976c62607576da7eaec81e5a6eb17a0ecc9b6e89528b57144c8b866a6531ca
|
||||
agent devops-automator 45dd972e3438c2fa4b52178080ed0e58da285f9495af1e6194ac06bc51b8f607
|
||||
agent devops-automator 9716ea2ff303718da6d6abbc8a234e78112d83ad2fc9f0e5399ef5451bfec55b
|
||||
agent discovery-coach c6dd139e5e70d296504a2de80c2d2af06e53c884a00a379e7f74bb7b1cf8a3e1
|
||||
agent document-generator 84e6ad2c86370deb62c0a86daff973f601788121fdb03b65d7f94d6bdf1afea6
|
||||
agent douyin-strategist 9a36813e6496f9ae416fe60dcb744a8ce3961641362b35fbb0f76dd194d29f99
|
||||
@@ -76,11 +76,11 @@ agent drupal-performance-engineer 772c9f1282e7776b3f28ab3a6b830a0260b6de4f814f91
|
||||
agent drupal-shopping-cart-engineer 00b3e2a42434e2d6fd19f8055e61162ba503e3a71a63597891ec7b91710f385f
|
||||
agent dx-engineer 2a966b23d47604b36dbbf2ce746ca69dab1a3be9116537fa0ef625346f1572d9
|
||||
agent economy-designer 359a80ea6bbd1c10a23d9f0acfdc045a5ae77aaadbb38c7ecc2a08e2b0ee4cf3
|
||||
agent email-intelligence-engineer ec8c5b0513e9163986f4682e15dd8e070d75b4413271abef52ef8a10106ae159
|
||||
agent email-intelligence-engineer c58ae528450270db72b03bc4695e86e828e099fc061e27f0a039aa01de4cdde2
|
||||
agent email-marketing-strategist a0f8db4f46c40da8ba1fa794159f1a4f3aaa9b6efee10169e08dd5bc62d514ab
|
||||
agent embedded-firmware-engineer 984c2eccab39eeee9bedf51a9d3ca4d340b8e72d7ff5ccfcc3ca6ff39903a1b4
|
||||
agent esg-sustainability-officer 075a8eb55e512c598ac92240b99f2f85cfa16f57e828b7bb336993adcdd38361
|
||||
agent evidence-collector 7250c568cbf4d622e4010f0db0f05e512a0b45a804a401736ccad7b679317259
|
||||
agent evidence-collector 56f713c696453f6e631a34dff50f82e1b7d81b609602ba8943ace590e96ef773
|
||||
agent executive-summary-generator b4a4ceb9ba860e223076a0144df62b8a25a4528b948e06020185e93c264e240a
|
||||
agent experiment-tracker c5d3f07b835b7e2e10aad874ede6e7a3cc365d1700aa70794014758dd9738865
|
||||
agent fedramp-rmf-compliance-engineer 48b72603d607cb425e0a2f25246e1d5c3a308c02a48289122bb546a5366c57eb
|
||||
@@ -89,7 +89,7 @@ agent feishu-integration-developer d59f8216b2de9409013b93934e43ad763e743303f3f78
|
||||
agent filament-optimization-specialist 11cabb3ba1f5325078e9d6871f6e40f32af94ca6c91cadbc494b2606a8b511ab
|
||||
agent finance-tracker d82f098a2cf90fdcb42743bcac76416b379b7b8f5255a4643fb7c04f7b2cef6f
|
||||
agent financial-analyst 5bc290a7ee437845c11d20d0c81ece9549c7659c0d701acd6238013df4e47f7c
|
||||
agent finops-engineer 8f749fd3d29a8d0891ea73fbcdde2702d2d229a2ce4c3017c188818957b9085f
|
||||
agent finops-engineer 2fd4275f09a76b8183256228cfb06d3d7313aa1a6ab2ab1ce6a8913291a11623
|
||||
agent focus-music-architect d5f7ad52721bc2557dde8cdeea661e742086ce328d49202051447c94c33f6b8e
|
||||
agent fp-a-analyst 765d403f720dd6ff2951fe8347ab3bca89d2c671e3d4eb355ba4dcc1743d3957
|
||||
agent french-consulting-market-navigator a776945e7ce36a0a807322c9897158a7ea6712b7571c319e0086c80aa58ec7e5
|
||||
@@ -102,7 +102,7 @@ agent geographer 68dedff8020bf80668e3343e9c14dced87191f4a669dda23c4ee0d5d840053e
|
||||
agent geoprocessing-specialist 4137f9deb327f880406b8ffc5fcf860f4c6ca49ea312735097004ad4eca203b9
|
||||
agent gis-analyst 095a1e6436a7da61070874198ec48ce9711ac5269882510fe6d4af38c90774e9
|
||||
agent gis-qa-engineer 130cdfad3a8091529eab10833d731bd0969b88f043268c6aad4b34cc3f4609dc
|
||||
agent git-workflow-master ad8db6bd75a1183dddce5e28e329a117b84410ea9191300dc7b90d0189c1f819
|
||||
agent git-workflow-master 6b22e6d1d39473b3a67cc8cbbb8b8238ec2184fa3f6605a75b1218cd01b17736
|
||||
agent global-podcast-strategist 0a0d65a7971af7cd439c383c4ffaf7d43d89ae808756e47391e047b0fed68323
|
||||
agent godot-gameplay-scripter 772157391d97f20a7fb02cc5008d9fc7e02871cda94a700bb328a6d3f352d3f9
|
||||
agent godot-multiplayer-engineer 3ef11d3a5cae9f42f12210f0737cac0ff0d6f40e80aa2b9f4f52e5155ca82216
|
||||
@@ -116,20 +116,20 @@ agent healthcare-marketing-compliance-specialist 5f2e709deff3066ee2e26a392118249
|
||||
agent historian 883638a108ce343ca5cc06febcf4b99d35142c6fc0be3b6605c8865d6722dea1
|
||||
agent hospitality-guest-services 503bf1b27fe2fb35e0a5e772d70943081c03093336fd32e157e9798f6b9fac87
|
||||
agent hr-onboarding 55ec220679bc78de1f3b094b643534077d1beaf80f98b263145caa97f4614d6d
|
||||
agent identity-access-engineer a225edd2a33ff902cddd776bc4bf25529296dfac0d5eddf25e70dfb73c359fcf
|
||||
agent identity-access-engineer 8af7fadd60e08a8929ea1f19ad48611d5862f099b20b83aac0ecda0d7edf04bc
|
||||
agent identity-graph-operator 565db70f50623d540e5af7883e2cbaff88715951e931693cde25d2e948680456
|
||||
agent image-prompt-engineer 6efdec2f800dc7182db7380c6b9981591893d78808ddbb2d12b6b4991af6351e
|
||||
agent incident-responder 426c5ba1c85d5ede51add9c16237836704a2bbf50c52c6327ca7358dacf6f989
|
||||
agent incident-response-commander 6c9585e10a072baf8e43153e33dfdacd98cca4e9dd8b2d69cb1bc095b58aebfd
|
||||
agent incident-responder 22680b884039638e991b64228dd6354e0617acd4b133bfed797ab157974fb521
|
||||
agent incident-response-commander 5fdf424e9cb991d4baf922328a6f0926cc4e064ed2ecf2f987e0761f5a0d1346
|
||||
agent inclusive-visuals-specialist 939821deb9f25ff9a14dcd18b3aee4036bdd00d0381c385b013ecaff1c9f236b
|
||||
agent infrastructure-maintainer 00159f8d363b241319b65e3a5a3fc87b3effdd18912d9e3436dc84c80b17998b
|
||||
agent instagram-curator 00a02db1ed88d56d456e91475a64c3f03e4cfdf3a0e9d19c031f9ece5328cf29
|
||||
agent internationalization-engineer 93aa0c58c78f9233d41e51581ce213bd98a4286bd8d03220c42c8e42a5dda3e6
|
||||
agent internationalization-engineer f39fce742bf04eb885adab011c4929b551c7de17a70e62a14ec454c615192719
|
||||
agent investment-researcher 0cb358dbad201073d75f75da528a80c36aa4610674ce3e925d42905584bb4a2b
|
||||
agent iot-fleet-engineer 0654db892579d6c0e796f53ec50ab04d784752ba0df9eb14de5eabe47b0569fc
|
||||
agent it-service-manager fa31fc63dd9c23d9f5f2a497a0d3d51ce4c7bdd090d10de606a3ae28632a37b2
|
||||
agent jira-workflow-steward 4cbba5f9ef76c5e25074c4204d3f6f86bbfff7ce4621b58b30e50266b0c8cb72
|
||||
agent knowledge-graph-engineer a71705683177a20c6398d4da7dc2689e19d93b5af2e8f6b3b5ba7df8f3ccc2ba
|
||||
agent knowledge-graph-engineer 18996402a347ff06fe4c84e4514994ae3a5ab2d38da2ec4ccb03affd61e73100
|
||||
agent korean-business-navigator cbaef5cc37130363a28ff901a11bb20ee39dceee94c4bdfc0ae9fe6f38e6ef22
|
||||
agent kuaishou-strategist fff4b32c0de544f47dbf1ce2329c70428399626288449de3d8318c428edc73d5
|
||||
agent language-translator 1c5acef0ff5ac8299e991bf470d07ad9d9d4d9e920afcec4d74aa1b70091e51c
|
||||
@@ -145,14 +145,14 @@ agent loan-officer-assistant 728dd25886c11a497cffbc15c9f0a5e9e2ad31d81dd0ab5d73c
|
||||
agent lsp-index-engineer b85f51e05fc7417725859cf7db2e75c689cdc83e9a0834491a21bfe90bf56f3c
|
||||
agent m-a-integration-manager 8b4fa0faafa68a917e74ea811d5a1dc498d96084339fc9e37bfb7cb83124f65e
|
||||
agent macos-spatial-metal-engineer 459c44c093c14db20c3e26e11486faf1b51a2ed2f5ccf1cdbab73de17308ff61
|
||||
agent master-plan-architect c861675824d79338d49f01dc04632bce755743a19ceefc02b4927b2bb17a7118
|
||||
agent mcp-builder b43870f2ed7bf26d8f67d3d9e7bcc77d85f4e1c11c8c032e204fad34352aed29
|
||||
agent master-plan-architect 181d5e2464dd1fb15e7951c2fff2c3b8feca97b1d066256ea9f26e27493c49e8
|
||||
agent mcp-builder 436091e1d1e7701f41700efae56a36c27ceab7bcd517e0f1e951153c663e4797
|
||||
agent medical-billing-coding-specialist 87637314fad64f8568209dcf95f00aac2374b9acddbb324b697d565029290cfd
|
||||
agent meeting-notes-specialist 2abd4b0e4b5d84d36306b7a5a01d1cae280ac03183cbeb7dcb271b5890e5d172
|
||||
agent minimal-change-engineer 3e712abbc8bf7e79ab4b7f955c95131bccd665efc064e439f47787dc7f27e23a
|
||||
agent mobile-app-builder 6047ed573ead2b152ed01ec9d241a4e4a8fe60342aaee3213e6d21025f84afcb
|
||||
agent mobile-release-engineer a36d473db28e070064d5f1ed75eff59cf36985a89b8985ee37bdc30ce47c0f24
|
||||
agent model-qa-specialist bbfe2a36835d325c36863ba53362867f1f5ec13322f16176dafeb4ca42b5cd92
|
||||
agent model-qa-specialist 416a9cdf19132984b8df2c905b1c4e98a0d0af9f9cad59a14aa7a2a786cf063d
|
||||
agent multi-agent-systems-architect 85ebc3de09b5619380454411a1c910bebf3e382b67a8920a01cd4d488fd9143a
|
||||
agent multi-platform-publisher 7190db08b2e29bed5b6df887d2cfa42143fc880f19bcf64f0064861cf22d05f4
|
||||
agent narrative-designer 2e487cbeaf61832a899f63398e6453f9d44d181a56ebc44e5a042a7c1c302e88
|
||||
@@ -165,10 +165,10 @@ agent orgscript-engineer a8cb7cea43d400f0c608aae00b4214419bdfc526f94075a55bd27dc
|
||||
agent outbound-strategist 3e28d6e7507f0c0c7c8b58ee1240458241d9dc25949f8bf9e95b2b0fab6ca181
|
||||
agent paid-media-auditor 2ae7e56aa2f9a8b06bab19f6c8560878aa2469afc96a125dcd31c0250515733a
|
||||
agent paid-social-strategist 76157e6b683ab4072f42834ea7d7f3f66c834f504e9178802a0be5fd955d5eb9
|
||||
agent payments-billing-engineer 8ff3a2d3ff29009cff13e6d9cfc770f67a76494831eb273f70388349e311a676
|
||||
agent pdf-engine-architect d38fe55ffb390460728e7a0738326c46fbc6e4063e320619ba832f7f1d865f3a
|
||||
agent payments-billing-engineer 92932901363c3d7e784bdef19e04a13faef5b2cd4173170910526e2bf434a578
|
||||
agent pdf-engine-architect 04d7366e40373caa669a09fa8cf7c5327e48a122d1ff3e2f78b40f8a327fa0fd
|
||||
agent penetration-tester 56d7934b013b2e11c4290d1316248c097122e092ebef5de76f62ed60b91ed5ba
|
||||
agent performance-benchmarker 225559d2d912646ef5bf70f20fdf6c5a77aff2acb8f0226b0aa4e8e6c7577824
|
||||
agent performance-benchmarker 408fe40fdc3bebb8b90a55b415aa9620abc7f446058010dc57bc0e7abe7a99e0
|
||||
agent persona-walkthrough-specialist 27f17ab9289480e277cd51ed96a95e04fb23e3c9f2cfef300cc940af6ffc8d44
|
||||
agent personal-growth-mentor 11a0955f8343b5ffb9c389e02c21e7101f6d21cea98ac294871c7590d51b1be4
|
||||
agent pipeline-analyst d2a3af6fa137fc02ff9910976d708390546be2aba772b3c15072d11f1e4356e1
|
||||
@@ -185,11 +185,11 @@ agent project-shepherd f366e437da8f1334948793965ff6de6b8bb00ca6a46814a2219346365
|
||||
agent prompt-engineer 5100207414ee407bfcc99520edfcc4cf71205b357ee771d6ec41dd39de1b1a31
|
||||
agent proposal-strategist 8586fd4d88a2dc4d865aed1a0b001772a6e414d85021ccae3b9493402d65024d
|
||||
agent psychologist 2305460ad8b493f8a60759b7b736ff6fd7e02f87cca3c4c70e15e20b59f62bef
|
||||
agent rag-pipeline-engineer f68905a75c93ecc2eb564a2adc3d68845ff04a22ce78ea3eb9ef1dd318393110
|
||||
agent rag-pipeline-engineer ea18e79982814e7e2a68803fb0db9a28ded2e5ccfefca853a549069de163e093
|
||||
agent rapid-prototyper 8f11bc93e1188cf78754862bdaa1540bc6c892e0ccc12e58bf6a91bdf86dd9a0
|
||||
agent real-estate-buyer-seller 4f2077bedf36c6128157cb81cdbe0b9c15b02999622e593d971022ff9e5775de
|
||||
agent reality-checker ac3f586d5ac39462ee5c3b89860dfeaddd3612544ac8367621a97973495fa226
|
||||
agent realtime-collaboration-engineer d05ff200171c9f0e46a6d3fe70b09f986eb9bf98f06c6140ac0b160354e8bfaa
|
||||
agent realtime-collaboration-engineer 6b730eab359407f375d8ad394f892174f9dddec963ed39f032828f4b0a5a2e56
|
||||
agent recruitment-specialist 851b5cad87722e10e40e9f1d2581ad78f7bded0ff9dee951deb003c5049b8fd3
|
||||
agent reddit-community-builder b8a189f0d247df6148ba046d942696cb34346a2359bf9b81d476b00e18e788ac
|
||||
agent report-distribution-agent 43930d1b0eadf06f99ad03b5bc434e32025208c172aec62888b4442291d4347b
|
||||
@@ -207,7 +207,7 @@ agent sales-outreach 625b0a5bfd7d0a0e2c3b85c0c3e3e8473da57c2e628b360a458e4095210
|
||||
agent salesforce-architect ca346376db8042f68861affa71dd4cade7c8c7b791351e0c8be0e2dc6781c531
|
||||
agent search-query-analyst fa26d7b5c018c13b3bec59887857a1a28c3c285ea419b963ce3dc43477af821e
|
||||
agent search-relevance-engineer d6ed38705708af22fd4d1b6ec817e9f46c897553ab132f70742bc72d92cc0435
|
||||
agent secrets-credential-hygiene-engineer 92cf9ecabc1cf2cdcc1aebe3c8e653c88f22051b8a5bc08975e32db638768a76
|
||||
agent secrets-credential-hygiene-engineer 574645c70294f161642e0ba2cd2e5bc9aefcc6b7ddc0b4e20cf8a741b2203ba4
|
||||
agent section-508-accessibility-specialist 1907cdc7cbd412401523a96fa940e86d9ee1966dc5e0cc38defefc901b11b392
|
||||
agent security-architect 9b339a90ac4f3e26fc40a6ce32911ba8db62a15b56ff5d10a3fb90cc37d30186
|
||||
agent senior-developer 697c44f1593cfa31b81a52596f01bd6d52170e1f8ba640a45c8fd668bed75b74
|
||||
@@ -235,11 +235,11 @@ agent support-responder c7fee67f6eed4b16002326dc4355ba341ed540b1be5ba42c93215200
|
||||
agent tax-strategist 4f0318fbf6f15f1041025fdf58deb0da1258e93b7c97e7387a82afc4fb2ba3a7
|
||||
agent technical-artist 49b07b6ab626c06d7d5e4b1fc7f6e1f6794f100e7a03d2b79b0d041855073e39
|
||||
agent technical-consultant 6b94aba5684f1f97dcc658ec023b7e646f88ed9257b6af642b7ad9f970e23000
|
||||
agent technical-writer 358b7bb56afc0a78c84184896b9310d5bc15f0ca5793290099df3dd7afc39f5d
|
||||
agent technical-writer 3d677d6f5a5c1ca2e3fa1ceefbce7e95c8a7812ec2345a972f4bd2bb6beca2cd
|
||||
agent terminal-integration-specialist 77404afbbbf384d091e4ba1e86610d0a40260626992e1e910795d4a7143e852c
|
||||
agent test-automation-engineer 1928169ccfcb12872e10f697824dc0b444ae695b889f2e3eda78ba4deda220e2
|
||||
agent test-results-analyzer 9ff4ba39e67ed23c6dad0de0f4ed8c06425d3eaca5944123a10fe2973f4d20c9
|
||||
agent threat-detection-engineer 2931ec321758ff5d4bf474ca512b2beed7c072a03356d3bce030fbd4e967f702
|
||||
agent test-automation-engineer 8789e56e01c02beb34d15fb1b957965ba39961baedb50f303279c148d566ffee
|
||||
agent test-results-analyzer 340668dc8782da6df908157d3cc63c7ebeb97e0b3f5224fbed9074d2c50cee13
|
||||
agent threat-detection-engineer ce4188addbb8f4a32c3912bf98b59e9bc9a81f692ef0a4f9f89cfa8f5844499c
|
||||
agent threat-intelligence-analyst 8137c504d3ba085151e4554e869696090544f710425c0155a36b3938e2c0d210
|
||||
agent tiktok-strategist 7e0c79b1196e61545706236b429c5520627b4f5d9a04b369f56877fd6802483e
|
||||
agent tool-evaluator 35be3ea9f0fe799c702225d7c09c1856eeb7909c95d9d0aeba4435b6ef0043dd
|
||||
@@ -252,28 +252,28 @@ agent unity-architect a1ddee16a9f82641fc5ebbc56f33f982926d0b87138acabf3583f0f010
|
||||
agent unity-editor-tool-developer 5204727b77a78916459207cd7d678a0496fea223329a123d99fa40ab667c7822
|
||||
agent unity-multiplayer-engineer 260a7ccc1cd8f9bd9029f240da96ccd8b138a036b789ea61e736afae703d1753
|
||||
agent unity-shader-graph-artist 8418d0e563195bb7e296cd4d6f9028b309bc295cf1f37fbf8e10acba8964c379
|
||||
agent universal-document-compiler 7b8f80f17f5fe823be8727c1f71412ee9105f81e369b839230bf2ce2b8107f70
|
||||
agent universal-document-compiler 969ab4e865cafd4545e522e911a05278ea5efa2bc882e1e974ebeb6bab0cc5f6
|
||||
agent unreal-multiplayer-architect b891806c48fee99c08d715b7fb5e5c1151d570d48b3390ad9917b0033c18cad8
|
||||
agent unreal-systems-engineer 57295f149a43fbd17a03793abbf91c29bc9b89079962598111a32ae6a5e75fab
|
||||
agent unreal-technical-artist d3cf3189d85bd5843007fc7abcd8b03b34447c6e2fb663e925c1341399184af0
|
||||
agent unreal-world-builder dfc9b1616e876387a5e9a1495d1c0de4b9001fb1082253fd7f076857e1a209b8
|
||||
agent uswds-developer c231a0464daac1ca3693d32fbb45a9e47e92f1103411194f4e4950264442c509
|
||||
agent ux-architect 94136fde96ddb23cb414b9108102dd5ebf2828f7faa731e37e747081d2a792ad
|
||||
agent ux-architect b6dcbccd85dc212136d35ae4b3f3f1d1e01ff04979a6c7c34289d91e09292d10
|
||||
agent ux-researcher f12ed95ef0a83912c5f8d6581b8770ca5506cf86de786d42f9b08d3a60773f67
|
||||
agent video-optimization-specialist 692a27cc151aec0f9ca513c8fc6373d7281b588121f3491bf8f1c058727aaae0
|
||||
agent video-streaming-engineer 584e612444dc48a2e769164fc2f6523e81a901054d58eeec1b3d0ff7ed0295fa
|
||||
agent video-streaming-engineer 79e0bec23332a65b7d0827d27e195718cb1a14991c756a7357800f47f08a711d
|
||||
agent visionos-spatial-engineer 725fec9ae38039d338453e5a9b6e47959d8046d29b2df9d14d49bd4ad08193c2
|
||||
agent visual-storyteller 7f96d968a54df0304e2592c45d9e96c005abb085ba07dcbb7ef52f6da9ae341c
|
||||
agent voice-ai-integration-engineer f5f37e76a990eec8165dc3036baa57ee26ab7e80ead1ab70653fce0b010633b1
|
||||
agent voice-ai-integration-engineer 98e66bf00214c516183503fd5088ec230242fefc08b8ef00421ad988bb1016d0
|
||||
agent web-gis-developer 03af81f34e7e835aaed3d40039e15bb06ce6f631dc65881af499a58b615714b5
|
||||
agent webassembly-engineer 186ff15a1d2f9ea575d035953a382709b47d5ab4542a6cf497b31230adfb5f52
|
||||
agent wechat-mini-program-developer b4ca454b1bb0d430029cf5fb33cc0cc663452c4bfd96aaf0ce1d578ec6dc134e
|
||||
agent wechat-mini-program-developer f9864c9def86dc9f906710b81e929d65c4a636cb50dd6c6d669e109a35a4fb2e
|
||||
agent wechat-official-account-manager 5225da2da8de3629d33516126f5baa2cf303ec6c36b23f00b6a5faa35afaa680
|
||||
agent weibo-strategist 8b2ae550969b402047370fdcc67383c48e0039326811c02c204cdc0ca0066699
|
||||
agent whimsy-injector 1f1d4ed575bdbe8d6f9878d062cffe7638b3ec25b2e6ca18660ac514b1dca093
|
||||
agent wordpress-performance-engineer e5648f29545c04eb9fa59f7dd6ae486f1ead83518da35b7cd1ccac7a9cb851b6
|
||||
agent wordpress-shopping-cart-engineer 55061d3f9d9e3b3326539964a1dc10e8dd9280988edfd470893e81e55f09ddf7
|
||||
agent workflow-architect 3a23681bded4c9573a234dda8771bf6a596d08dd83e193122cc1e8bcd8670f95
|
||||
agent workflow-architect f8ef420a2f211bea092c08f65946b3bd424933d990de29514c7ede6b07becd03
|
||||
agent workflow-optimizer d9d871f40edb0bf57a36766422342f39d40662a55dedfc5160d5773f43adc8c3
|
||||
agent x-twitter-intelligence-analyst 5aac652ec86bf81addde3271ef2f3c73f3dec31d8ef1acab85e6bb228331062f
|
||||
agent xiaohongshu-specialist 51ba0e33a86db9a79515e8a77a36e9c67cf46528899c4f1bef3f70ccaec70639
|
||||
|
||||
+17
-3
@@ -6,6 +6,7 @@
|
||||
# converted files to integrations/<tool>/. Run this to regenerate all
|
||||
# integration files after adding or modifying agents.
|
||||
#
|
||||
# --- USAGE-START --- (sentinel for usage(); do not remove)
|
||||
# Usage:
|
||||
# ./scripts/convert.sh [--tool <name>] [--out <dir>] [--parallel] [--jobs N] [--help]
|
||||
#
|
||||
@@ -30,8 +31,12 @@
|
||||
# Output is written to integrations/<tool>/ relative to the repo root.
|
||||
# This script never touches user config dirs — see install.sh for that.
|
||||
#
|
||||
# --tool <name> Convert for one tool (default: all).
|
||||
# --out <dir> Write to <dir>/<tool>/ instead of integrations/<tool>/.
|
||||
# --parallel When tool is 'all', run independent tools in parallel (output order may vary).
|
||||
# --jobs N Max parallel jobs when using --parallel (default: nproc or 4).
|
||||
#
|
||||
# --- USAGE-END --- (sentinel for usage(); do not remove)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -77,9 +82,18 @@ AGENT_DIRS=(
|
||||
)
|
||||
|
||||
# --- Usage ---
|
||||
# usage [status] — print the header between the USAGE sentinels and exit.
|
||||
# `--help` exits 0 on stdout; an unknown option exits 1 with the text on
|
||||
# stderr. The old hard-coded `sed -n '3,28p'` stopped above --parallel,
|
||||
# --jobs and --out, and exiting 0 after "Unknown option" meant a mistyped
|
||||
# flag in CI or a wrapper script read as success.
|
||||
usage() {
|
||||
sed -n '3,28p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit 0
|
||||
local status="${1:-0}"
|
||||
local text
|
||||
text="$(sed -n '/^# --- USAGE-START ---/,/^# --- USAGE-END ---/p' "$0" \
|
||||
| sed -e '1d;$d' -e 's/^# \{0,1\}//')"
|
||||
if (( status == 0 )); then printf '%s\n' "$text"; else printf '%s\n' "$text" >&2; fi
|
||||
exit "$status"
|
||||
}
|
||||
|
||||
# Default parallel job count (nproc on Linux; sysctl on macOS when nproc missing)
|
||||
@@ -793,7 +807,7 @@ main() {
|
||||
--parallel) use_parallel=true; shift ;;
|
||||
--jobs) parallel_jobs="${2:?'--jobs requires a value'}"; shift 2 ;;
|
||||
--help|-h) usage ;;
|
||||
*) error "Unknown option: $1"; usage ;;
|
||||
*) error "Unknown option: $1"; usage 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
|
||||
+122
-48
@@ -33,8 +33,9 @@
|
||||
# Selection (compose freely; empty = everything):
|
||||
# --tool <a,b> Only these tools
|
||||
# --division <a,b> Only these teams/divisions (comma-separated)
|
||||
# --agent <slug,slug> Only these specific agents
|
||||
# --agents-file <path> Agents listed in a file (one slug/name per line, # comments ok)
|
||||
# --agent <id,id> Only these specific agents (install slug, display name,
|
||||
# or file stem such as engineering-frontend-developer)
|
||||
# --agents-file <path> Agents listed in a file (one id per line, # comments ok)
|
||||
#
|
||||
# Mode:
|
||||
# --link Symlink instead of copy (updates propagate)
|
||||
@@ -165,6 +166,7 @@ AGENTS_FILE="" # --agents-file
|
||||
DRY_RUN=false # --dry-run
|
||||
SELECTION_ACTIVE=false # true once any agent-level filter is applied
|
||||
_ALLOWED_SLUGS="" # newline-delimited cache of allowed slugs
|
||||
_ROSTER_INDEX="" # "<install slug>\t<file stem>" per agent; see roster_index
|
||||
|
||||
# division_files <division> — agent file paths (frontmatter only) in a division.
|
||||
division_files() {
|
||||
@@ -178,16 +180,43 @@ division_files() {
|
||||
# division_count <division> — number of agents in a division.
|
||||
division_count() { division_files "$1" | grep -c . ; }
|
||||
|
||||
# agent_slug_exists <slug> — verify a requested agent against the source roster.
|
||||
# Selection filters should fail before installation when they name nothing that
|
||||
# can be installed; otherwise dry-run counts and completion messages lie.
|
||||
agent_slug_exists() {
|
||||
local target="$1" div f
|
||||
# roster_index — fill _ROSTER_INDEX with one "<install slug>\t<file stem>" line
|
||||
# per agent, once. Call it in the parent shell before resolve_agent: a $(...)
|
||||
# caller would build its own copy and throw it away.
|
||||
#
|
||||
# Resolving each requested agent used to rescan the roster, running get_field
|
||||
# on all 279 files per request, so a 36-agent runbook roster cost ~10,000
|
||||
# get_field calls before anything installed.
|
||||
roster_index() {
|
||||
[[ -n "$_ROSTER_INDEX" ]] && return 0
|
||||
local div f
|
||||
for div in "${ALL_DIVISIONS[@]}"; do
|
||||
while IFS= read -r f; do
|
||||
[[ "$(agent_slug "$f")" == "$target" ]] && return 0
|
||||
_ROSTER_INDEX+="$(agent_slug "$f")"$'\t'"$(basename "$f" .md)"$'\n'
|
||||
done < <(division_files "$div")
|
||||
done
|
||||
}
|
||||
|
||||
# resolve_agent <requested> — print the install slug for a requested agent,
|
||||
# 1 if nothing matches. Selection filters should fail before installation when
|
||||
# they name nothing that can be installed; otherwise dry-run counts and
|
||||
# completion messages lie.
|
||||
#
|
||||
# Two spellings name an agent. The install slug comes from `name:` and is what
|
||||
# --list agents prints. The file stem is the corpus id strategy/runbooks.json
|
||||
# uses ("engineering-frontend-developer"), and for 206 of 279 agents it is not
|
||||
# the slug, so 35 of the 36 agents the runbooks list could not be selected by
|
||||
# the ids the runbooks give. Slugs are tried first; no stem equals another
|
||||
# agent's slug today, and slug-first keeps it unambiguous if one ever does.
|
||||
resolve_agent() {
|
||||
local target="$1" slug stem
|
||||
[[ -n "$target" ]] || return 1
|
||||
while IFS=$'\t' read -r slug stem; do
|
||||
[[ -n "$slug" && "$slug" == "$target" ]] && { printf '%s' "$slug"; return 0; }
|
||||
done <<< "$_ROSTER_INDEX"
|
||||
while IFS=$'\t' read -r slug stem; do
|
||||
[[ -n "$slug" && "$stem" == "$target" ]] && { printf '%s' "$slug"; return 0; }
|
||||
done <<< "$_ROSTER_INDEX"
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -199,7 +228,8 @@ build_selection() {
|
||||
return
|
||||
fi
|
||||
SELECTION_ACTIVE=true
|
||||
local slugs="" div f s line requested
|
||||
local slugs="" div f s line requested resolved
|
||||
roster_index
|
||||
for div in ${FILTER_DIVISIONS[@]+"${FILTER_DIVISIONS[@]}"}; do
|
||||
while IFS= read -r f; do
|
||||
s="$(agent_slug "$f")"; [[ -n "$s" ]] && slugs+="$s"$'\n'
|
||||
@@ -207,11 +237,11 @@ build_selection() {
|
||||
done
|
||||
for s in ${FILTER_AGENTS[@]+"${FILTER_AGENTS[@]}"}; do
|
||||
requested="$(slugify "$s")"
|
||||
if ! agent_slug_exists "$requested"; then
|
||||
if ! resolved="$(resolve_agent "$requested")"; then
|
||||
err "Unknown agent '$s'. Use --list agents to see the available roster."
|
||||
exit 1
|
||||
fi
|
||||
slugs+="$requested"$'\n'
|
||||
slugs+="$resolved"$'\n'
|
||||
done
|
||||
if [[ -n "$AGENTS_FILE" ]]; then
|
||||
[[ -f "$AGENTS_FILE" ]] || { err "agents-file not found: $AGENTS_FILE"; exit 1; }
|
||||
@@ -220,11 +250,11 @@ build_selection() {
|
||||
line="$(printf '%s' "$line" | xargs 2>/dev/null)" # trim
|
||||
[[ -z "$line" ]] && continue
|
||||
requested="$(slugify "$line")"
|
||||
if ! agent_slug_exists "$requested"; then
|
||||
if ! resolved="$(resolve_agent "$requested")"; then
|
||||
err "Unknown agent '$line' in agents-file '$AGENTS_FILE'."
|
||||
exit 1
|
||||
fi
|
||||
slugs+="$requested"$'\n'
|
||||
slugs+="$resolved"$'\n'
|
||||
done < "$AGENTS_FILE"
|
||||
fi
|
||||
_ALLOWED_SLUGS="$(printf '%s' "$slugs" | sort -u | sed '/^$/d')"
|
||||
@@ -283,26 +313,30 @@ OVERRIDE_PATH="" # --path (single-destination override)
|
||||
|
||||
# install_file <src> <dest> — copy, or symlink when --link is set.
|
||||
install_file() {
|
||||
if $USE_LINK; then
|
||||
ln -sf "$1" "$2"
|
||||
else
|
||||
local target="$2"
|
||||
[[ -d "$target" ]] && target="${target%/}/$(basename "$1")"
|
||||
if [[ -L "$target" ]]; then
|
||||
# cp would follow the link and overwrite whatever it points at.
|
||||
local link_to; link_to="$(readlink "$target")"
|
||||
if [[ "$link_to" == "$REPO_ROOT/"* ]]; then
|
||||
# Our own --link install: switching to a copy is the intended change.
|
||||
rm -f -- "$target"
|
||||
else
|
||||
# Someone else's link: leave it and its target alone, keep installing
|
||||
# the rest, and say so in the summary (one stray link must not abort
|
||||
# the install halfway through the roster).
|
||||
warn "Skipped $target — it is a symlink to $link_to; not overwriting it."
|
||||
[[ -n "${SKIPPED_LOG:-}" ]] && printf '%s -> %s\n' "$target" "$link_to" >> "$SKIPPED_LOG"
|
||||
return 0
|
||||
fi
|
||||
local target="$2"
|
||||
# Directory destinations have a trailing slash. Do not follow a leaf
|
||||
# symlink to a directory when deciding which file belongs to the installer.
|
||||
if [[ "$target" == */ ]] || { ! $USE_LINK && [[ -d "$target" ]]; }; then
|
||||
target="${target%/}/$(basename "$1")"
|
||||
fi
|
||||
if [[ -L "$target" ]]; then
|
||||
local link_to; link_to="$(readlink "$target")"
|
||||
if [[ "$link_to" == "$REPO_ROOT/"* ]]; then
|
||||
# An installer-owned link may be refreshed or switched to a copy.
|
||||
rm -f -- "$target"
|
||||
else
|
||||
warn "Skipped $target — it is a symlink to $link_to; not overwriting it."
|
||||
[[ -n "${SKIPPED_LOG:-}" ]] && printf '%s -> %s\n' "$target" "$link_to" >> "$SKIPPED_LOG"
|
||||
return 0
|
||||
fi
|
||||
elif $USE_LINK && [[ -e "$target" ]]; then
|
||||
warn "Skipped $target — it already exists; not replacing it with a symlink."
|
||||
[[ -n "${SKIPPED_LOG:-}" ]] && printf '%s (existing file)\n' "$target" >> "$SKIPPED_LOG"
|
||||
return 0
|
||||
fi
|
||||
if $USE_LINK; then
|
||||
ln -s "$1" "$target"
|
||||
else
|
||||
cp "$1" "$2"
|
||||
fi
|
||||
}
|
||||
@@ -311,12 +345,20 @@ install_file() {
|
||||
# path_collision_group <tool> — tools in the same group write identical
|
||||
# filenames into a shared --path and would overwrite each other; empty means
|
||||
# the tool's output is distinct and may share a path with anything. Derived by
|
||||
# installing one agent with every tool into a sandbox and comparing what
|
||||
# landed; re-measure if a converter's output naming changes.
|
||||
# installing agents with every tool into a sandbox and comparing what landed;
|
||||
# re-measure if a converter's output naming changes.
|
||||
#
|
||||
# claude-code and copilot copy the source file under its own name. For most
|
||||
# agents that is <division>-<slug>.md, but 73 of 279 are named <slug>.md
|
||||
# already (all of game-development/, most of specialized/), and for those the
|
||||
# name is exactly what gemini-cli, opencode, qwen and zcode write. Measuring
|
||||
# with one engineering agent missed that, so `--tool claude-code,qwen --path X`
|
||||
# reported both installs OK while qwen overwrote the Claude Code file. One
|
||||
# group, because a full install collides on 73 files, not zero.
|
||||
path_collision_group() {
|
||||
case "$1" in
|
||||
claude-code|copilot) printf 'raw-source-md' ;; # <division>-<slug>.md
|
||||
gemini-cli|opencode|qwen|zcode) printf 'slug-md' ;; # <slug>.md
|
||||
claude-code|copilot|gemini-cli|opencode|qwen|zcode)
|
||||
printf 'agent-md' ;; # <slug>.md, or the source's name
|
||||
antigravity|osaurus|dsh) printf 'agency-skill' ;; # agency-<slug>/SKILL.md
|
||||
*) printf '' ;;
|
||||
esac
|
||||
@@ -456,9 +498,14 @@ usage() {
|
||||
# (excluding the sentinel lines themselves) and strip the leading "# ".
|
||||
# Using sentinels instead of hard-coded line numbers means adding lines
|
||||
# to the header comment block won't silently break --help output.
|
||||
sed -n '/^# --- USAGE-START ---/,/^# --- USAGE-END ---/p' "$0" \
|
||||
| sed -e '1d;$d' -e 's/^# \{0,1\}//'
|
||||
exit 0
|
||||
# An unknown option passes 1: the text goes to stderr and the exit is
|
||||
# non-zero, so a mistyped flag in CI or a wrapper script is not a success.
|
||||
local status="${1:-0}"
|
||||
local text
|
||||
text="$(sed -n '/^# --- USAGE-START ---/,/^# --- USAGE-END ---/p' "$0" \
|
||||
| sed -e '1d;$d' -e 's/^# \{0,1\}//')"
|
||||
if (( status == 0 )); then printf '%s\n' "$text"; else printf '%s\n' "$text" >&2; fi
|
||||
exit "$status"
|
||||
}
|
||||
|
||||
# Default parallel job count (nproc on Linux; sysctl on macOS when nproc missing)
|
||||
@@ -1553,7 +1600,7 @@ main() {
|
||||
--parallel) use_parallel=true; shift ;;
|
||||
--jobs) parallel_jobs="${2:?'--jobs requires a value'}"; shift 2 ;;
|
||||
--help|-h) usage ;;
|
||||
*) err "Unknown option: $1"; usage ;;
|
||||
*) err "Unknown option: $1"; usage 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
@@ -1676,18 +1723,20 @@ main() {
|
||||
fi
|
||||
printf "\n"
|
||||
|
||||
local installed=0 t i=0
|
||||
local installed=0 t i=0 rc
|
||||
local failed=()
|
||||
if $use_parallel; then
|
||||
local install_out_dir
|
||||
local install_out_dir install_status=0
|
||||
install_out_dir="$(mktemp -d)"
|
||||
export AGENCY_INSTALL_OUT_DIR="$install_out_dir"
|
||||
export AGENCY_INSTALL_SCRIPT="$SCRIPT_DIR/install.sh"
|
||||
export AGENCY_INSTALL_EXTRA="$(worker_flags)"
|
||||
printf '%s\n' "${SELECTED_TOOLS[@]}" | xargs -P "$parallel_jobs" -I {} sh -c 'AGENCY_INSTALL_WORKER=1 "$AGENCY_INSTALL_SCRIPT" --tool "{}" --no-interactive $AGENCY_INSTALL_EXTRA > "$AGENCY_INSTALL_OUT_DIR/{}" 2>&1'
|
||||
printf '%s\n' "${SELECTED_TOOLS[@]}" | xargs -P "$parallel_jobs" -I {} sh -c 'AGENCY_INSTALL_WORKER=1 "$AGENCY_INSTALL_SCRIPT" --tool "{}" --no-interactive $AGENCY_INSTALL_EXTRA > "$AGENCY_INSTALL_OUT_DIR/{}" 2>&1' || install_status=$?
|
||||
for t in "${SELECTED_TOOLS[@]}"; do
|
||||
[[ -f "$install_out_dir/$t" ]] && cat "$install_out_dir/$t"
|
||||
done
|
||||
rm -rf "$install_out_dir"
|
||||
[[ "$install_status" -eq 0 ]] || return "$install_status"
|
||||
installed=$n_selected
|
||||
else
|
||||
for t in "${SELECTED_TOOLS[@]}"; do
|
||||
@@ -1695,25 +1744,50 @@ main() {
|
||||
progress_bar "$i" "$n_selected"
|
||||
printf "\n"
|
||||
printf " ${C_DIM}[%s/%s]${C_RESET} %s\n" "$i" "$n_selected" "$t"
|
||||
install_tool "$t"
|
||||
(( installed++ )) || true
|
||||
# One tool failing must not cost the tools after it. A bare
|
||||
# install_tool under set -e exited the whole script at the first
|
||||
# `return 1`, so a missing integrations/cursor meant qwen, codex and
|
||||
# every later tool were never tried and nothing said so.
|
||||
#
|
||||
# Not `install_tool "$t" || ...`: bash ignores errexit inside anything
|
||||
# run on the left of || (subshell included), so a failing cp inside a
|
||||
# tool would carry on as if it had worked. The subshell turns errexit
|
||||
# back on for itself while the parent's is off for this one command.
|
||||
set +e
|
||||
( set -e; install_tool "$t" )
|
||||
rc=$?
|
||||
set -e
|
||||
if (( rc == 0 )); then
|
||||
(( installed++ )) || true
|
||||
else
|
||||
failed+=("$t")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Done box
|
||||
local msg=" Done! Installed $installed tool(s)."
|
||||
(( ${#failed[@]} )) && msg=" Installed $installed of $n_selected tool(s)."
|
||||
printf "\n"
|
||||
box_top
|
||||
box_row "${C_GREEN}${C_BOLD}${msg}${C_RESET}"
|
||||
if (( ${#failed[@]} )); then
|
||||
box_row "${C_YELLOW}${C_BOLD}${msg}${C_RESET}"
|
||||
else
|
||||
box_row "${C_GREEN}${C_BOLD}${msg}${C_RESET}"
|
||||
fi
|
||||
box_bot
|
||||
printf "\n"
|
||||
if [[ -s "$SKIPPED_LOG" ]]; then
|
||||
warn "Not installed: $(wc -l < "$SKIPPED_LOG" | tr -d ' ') file(s) whose destination is a symlink to somewhere else:"
|
||||
warn "Not installed: $(wc -l < "$SKIPPED_LOG" | tr -d ' ') file(s) whose destination is an existing user file or foreign symlink:"
|
||||
sed 's/^/ /' "$SKIPPED_LOG" >&2
|
||||
warn "Remove or replace those links, then re-run to install them."
|
||||
warn "Move or remove those destinations, then re-run to install them."
|
||||
fi
|
||||
dim " Run ./scripts/convert.sh to regenerate after adding or editing agents."
|
||||
printf "\n"
|
||||
if (( ${#failed[@]} )); then
|
||||
err "Failed: ${failed[*]} — see the [ERR] line under each above. The other tools installed."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+12
-2
@@ -77,17 +77,27 @@ fence_open_p() {
|
||||
}
|
||||
|
||||
# fence_closes_p <line> <open_marker> <open_len> <open_indent> — 0 if <line>
|
||||
# closes the open fence (same char, run len >= open, indent <= open); 1
|
||||
# otherwise, including non-fence lines (callers need not pre-classify).
|
||||
# closes the open fence (same char, run len >= open, indent <= open, nothing
|
||||
# but whitespace after the run); 1 otherwise, including non-fence lines
|
||||
# (callers need not pre-classify).
|
||||
#
|
||||
# The "nothing after the run" part is CommonMark's rule, and GitHub renders by
|
||||
# it: inside an open ``` block, a "```python" line is content, not a closer
|
||||
# and not a nested opener. Accepting it as a closer made these helpers read a
|
||||
# ```markdown template holding a ```bash example as two short blocks, while
|
||||
# GitHub saw one block that closed at the example's bare ``` — so a ## line
|
||||
# the split treated as code rendered as a heading, and the reverse.
|
||||
fence_closes_p() {
|
||||
local line="$1" open_marker="$2" open_len="$3" open_indent="$4"
|
||||
local re='^( {0,3})(`{3,}|~{3,})'
|
||||
[[ "$line" =~ $re ]] || return 1
|
||||
local close_indent=${#BASH_REMATCH[1]}
|
||||
local close_run="${BASH_REMATCH[2]}"
|
||||
local rest="${line:${#BASH_REMATCH[0]}}"
|
||||
[[ "${close_run:0:1}" == "$open_marker" ]] || return 1
|
||||
(( ${#close_run} >= open_len )) || return 1
|
||||
(( close_indent <= open_indent )) || return 1
|
||||
[[ -z "${rest//[[:space:]]/}" ]] || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
+38
-3
@@ -112,6 +112,15 @@ lint_file() {
|
||||
return
|
||||
fi
|
||||
|
||||
# The lightweight converters read plain or quoted scalar fields. A folded
|
||||
# block otherwise passes presence checks but leaks its YAML indicator into
|
||||
# the generated value.
|
||||
if grep -qE '^[[:space:]]*[[:alnum:]_-]+:[[:space:]]*>([-+][1-9]?|[1-9][-+]?)?([[:space:]]+#.*)?[[:space:]]*$' <<<"$frontmatter"; then
|
||||
echo "ERROR $file: folded YAML frontmatter is unsupported — use a single-line scalar"
|
||||
errors=$((errors + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
# 2. Check required frontmatter fields
|
||||
for field in "${REQUIRED_FRONTMATTER[@]}"; do
|
||||
if ! grep -qE -- "^${field}:" <<<"$frontmatter"; then
|
||||
@@ -162,8 +171,15 @@ lint_file() {
|
||||
|
||||
local soul_headers=0
|
||||
local agents_headers=0
|
||||
local fence_marker="" fence_len=0 fence_indent=0
|
||||
while IFS= read -r line; do
|
||||
local fence_marker="" fence_len=0 fence_indent=0 fence_line=0
|
||||
# Walk the body from the file itself rather than from $body, so fence
|
||||
# errors can name a real line number. ($body drops every "---" line.)
|
||||
local lineno
|
||||
lineno=$(awk 'NR > 1 && $0 == "---" {print NR; exit}' "$file")
|
||||
# "|| [[ -n $line ]]" keeps a last line that has no trailing newline; several
|
||||
# agents end on a closing fence with none, and dropping it reads as unclosed.
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
lineno=$((lineno + 1))
|
||||
# Skip fenced code blocks so ## doc-comment lines (e.g. GDScript `##`)
|
||||
# and in-fence markdown headers aren't miscounted (issue #849).
|
||||
if [[ -n "$fence_marker" ]]; then
|
||||
@@ -171,6 +187,17 @@ lint_file() {
|
||||
fence_marker=""
|
||||
fence_len=0
|
||||
fence_indent=0
|
||||
elif fence_open_p "$line" \
|
||||
&& [[ "${BASH_REMATCH[2]:0:1}" == "$fence_marker" ]] \
|
||||
&& (( ${#BASH_REMATCH[2]} >= fence_len )); then
|
||||
# A fence line long enough to close this block that did not close it
|
||||
# can only be one with an info string: someone nesting ```bash inside
|
||||
# a ```markdown template. Markdown has no nesting at equal length —
|
||||
# GitHub shows that line as text and ends the outer block at the next
|
||||
# bare ```, so the rest of the template renders as a document.
|
||||
echo "ERROR $file:$lineno: '${line}' inside the block opened at line $fence_line does not nest — the next bare ${fence_marker}${fence_marker}${fence_marker} closes the outer block instead"
|
||||
echo " fence the outer block with a longer run (e.g. ${fence_marker}${fence_marker}${fence_marker}${fence_marker}markdown ... ${fence_marker}${fence_marker}${fence_marker}${fence_marker}) so the inner ones stay inside it"
|
||||
errors=$((errors + 1))
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
@@ -178,6 +205,7 @@ lint_file() {
|
||||
fence_marker="${BASH_REMATCH[2]:0:1}"
|
||||
fence_len=${#BASH_REMATCH[2]}
|
||||
fence_indent=${#BASH_REMATCH[1]}
|
||||
fence_line=$lineno
|
||||
continue
|
||||
fi
|
||||
if [[ "$line" =~ ^##[[:space:]] ]]; then
|
||||
@@ -191,7 +219,14 @@ lint_file() {
|
||||
agents_headers=$((agents_headers + 1))
|
||||
fi
|
||||
fi
|
||||
done <<< "$body"
|
||||
done < <(tail -n +"$((lineno + 1))" "$file")
|
||||
|
||||
# An unclosed block runs to the end of the file: on GitHub, and in every
|
||||
# tool the converters feed, everything after the opener renders as code.
|
||||
if [[ -n "$fence_marker" ]]; then
|
||||
echo "ERROR $file:$fence_line: code block is never closed — everything after line $fence_line renders as code"
|
||||
errors=$((errors + 1))
|
||||
fi
|
||||
|
||||
if [[ $soul_headers -eq 0 ]]; then
|
||||
echo "WARN $file: no section headers map to SOUL.md in convert.sh"
|
||||
|
||||
@@ -41,4 +41,33 @@ output="$($INSTALLER --tool claude-code --agent 'Developer Tooling Engineer' --d
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "PASS: install.sh rejects unknown agent selections and accepts display names"
|
||||
# The file stem is the id strategy/runbooks.json uses, and for most agents it is
|
||||
# not the install slug (engineering-frontend-developer vs frontend-developer).
|
||||
output="$("$INSTALLER" --tool claude-code --agent engineering-frontend-developer --dry-run 2>&1)"
|
||||
[[ "$output" == *"Agents: 1"* ]] || {
|
||||
printf 'File-stem selection did not resolve to one agent:\n%s\n' "$output" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Every runbook roster, fed to --agents-file as the runbooks list it, resolves
|
||||
# to exactly its own agents. On main 35 of the 36 ids were "Unknown agent".
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
while IFS=$'\t' read -r runbook count ids; do
|
||||
printf '%s\n' $ids > "$AGENTS_FILE"
|
||||
set +e
|
||||
output="$("$INSTALLER" --tool claude-code --agents-file "$AGENTS_FILE" --dry-run 2>&1)"
|
||||
status=$?
|
||||
set -e
|
||||
[[ "$status" -eq 0 && "$output" == *"Agents: $count"* ]] || {
|
||||
printf 'Runbook %s roster (%s agents) did not resolve:\n%s\n' "$runbook" "$count" "$output" >&2
|
||||
exit 1
|
||||
}
|
||||
done < <(python3 - "$REPO_ROOT/strategy/runbooks.json" <<'PY'
|
||||
import json, sys
|
||||
for rb in json.load(open(sys.argv[1], encoding="utf-8"))["runbooks"]:
|
||||
ids = sorted({a for group in rb["roster"] for a in group["agents"]})
|
||||
print(f'{rb["slug"]}\t{len(ids)}\t{" ".join(ids)}')
|
||||
PY
|
||||
)
|
||||
|
||||
echo "PASS: install.sh rejects unknown agent selections and accepts display names, file stems, and runbook rosters"
|
||||
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# convert.sh and install.sh: --help documents every option and exits 0; an
|
||||
# unknown option exits non-zero with the usage text on stderr.
|
||||
#
|
||||
# Both scripts used to call usage() after "Unknown option", and usage() always
|
||||
# exited 0, so `convert.sh --tol codex` in CI or a wrapper read as success.
|
||||
# convert.sh's --help also printed a hard-coded line range that stopped above
|
||||
# --parallel, --jobs and --out.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
tmp="$(mktemp -d "${TMPDIR:-/tmp}/agency-cli-usage.XXXXXX")"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||
|
||||
for script in convert.sh install.sh; do
|
||||
status=0
|
||||
bash "$SCRIPT_DIR/$script" --no-such-flag > "$tmp/out" 2> "$tmp/err" || status=$?
|
||||
[[ "$status" -ne 0 ]] || fail "$script --no-such-flag exited 0"
|
||||
grep -q 'Unknown option: --no-such-flag' "$tmp/err" || fail "$script did not name the unknown option on stderr"
|
||||
grep -q 'Usage:' "$tmp/err" || fail "$script did not print usage on stderr for an unknown option"
|
||||
[[ ! -s "$tmp/out" ]] || fail "$script wrote to stdout for an unknown option"
|
||||
|
||||
bash "$SCRIPT_DIR/$script" --help > "$tmp/help" 2>&1 || fail "$script --help exited non-zero"
|
||||
grep -q 'Usage:' "$tmp/help" || fail "$script --help printed no usage"
|
||||
! grep -q 'USAGE-START\|USAGE-END' "$tmp/help" || fail "$script --help printed its sentinel lines"
|
||||
done
|
||||
|
||||
for opt in --tool --out --parallel --jobs; do
|
||||
grep -q -- "^ $opt " <(bash "$SCRIPT_DIR/convert.sh" --help) \
|
||||
|| fail "convert.sh --help does not describe $opt"
|
||||
done
|
||||
|
||||
echo "PASS: unknown options exit non-zero with usage on stderr; --help documents every convert.sh option"
|
||||
@@ -330,7 +330,7 @@ elif not colour_bad:
|
||||
# block in half and leaves each file holding a dangling fence, which renders as
|
||||
# broken markdown for every user of that integration (#849). So every fenced
|
||||
# block in a source must land intact in exactly one of the two files.
|
||||
SPLIT_FENCE = re.compile(r"^(`{3,}|~{3,})")
|
||||
SPLIT_FENCE = re.compile(r"^(`{3,}|~{3,})(.*)$")
|
||||
|
||||
def body_lines(text):
|
||||
"""Mirror lib.sh's get_body, including `$(...)`'s trailing-newline strip."""
|
||||
@@ -352,10 +352,11 @@ def fence_blocks(lines):
|
||||
m = SPLIT_FENCE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
tok = m.group(1)
|
||||
tok, rest = m.group(1), m.group(2)
|
||||
if not marker:
|
||||
marker, mlen, start = tok[0], len(tok), i
|
||||
elif tok[0] == marker and len(tok) >= mlen:
|
||||
elif tok[0] == marker and len(tok) >= mlen and not rest.strip():
|
||||
# only a bare run closes: "```bash" inside a block is content (lib.sh fence_closes_p)
|
||||
res.append((start, i)); marker, mlen, start = "", 0, None
|
||||
if marker and start is not None:
|
||||
res.append((start, len(lines) - 1))
|
||||
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
scratch="$(mktemp -d)"
|
||||
trap 'rm -rf "$scratch"' EXIT
|
||||
for indicator in '>' '>-' '>+' '>2' '>2-' '>-2' '>+2' '>2+ # comment'; do
|
||||
printf '%s\n' '---' 'name: Example Agent' "description: $indicator" ' First line' ' Second line' 'color: blue' '---' '## Identity' '## Core Mission' '## Critical Rules' > "$scratch/agent.md"
|
||||
if bash "$SCRIPT_DIR/lint-agents.sh" "$scratch/agent.md" > "$scratch/result.log" 2>&1; then
|
||||
echo "FAIL: linter accepted folded frontmatter $indicator" >&2; exit 1
|
||||
fi
|
||||
grep -q 'folded YAML frontmatter is unsupported' "$scratch/result.log"
|
||||
done
|
||||
for description in '"A > B"' "'A > B'" 'Ordinary description'; do
|
||||
printf '%s\n' '---' 'name: Example Agent' "description: $description" 'color: blue' '---' '## Identity' '## Core Mission' '## Critical Rules' '> Body quotation' > "$scratch/agent.md"
|
||||
bash "$SCRIPT_DIR/lint-agents.sh" "$scratch/agent.md" > "$scratch/result.log" 2>&1
|
||||
done
|
||||
echo 'PASS: folded frontmatter is rejected while quoted scalars and body quotations pass'
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
# One tool failing in a sequential install must not cost the tools after it,
|
||||
# and the install must still exit non-zero and name the tool that failed.
|
||||
#
|
||||
# Before: install_tool ran bare under set -e, so the first tool whose output
|
||||
# was missing exited the script. With gemini-cli, cursor and qwen selected and
|
||||
# no integrations/cursor, gemini-cli installed, cursor printed its [ERR], and
|
||||
# qwen was never attempted and never mentioned.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
tmp="$(mktemp -d "${TMPDIR:-/tmp}/agency-continue.XXXXXX")"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
fail() { echo "FAIL: $*" >&2; sed 's/^/ /' "$tmp/output" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$tmp/repo/scripts" "$tmp/repo/engineering" \
|
||||
"$tmp/repo/integrations/gemini-cli/agents" "$tmp/repo/integrations/qwen/agents" \
|
||||
"$tmp/home/project"
|
||||
cp "$SCRIPT_DIR/install.sh" "$SCRIPT_DIR/lib.sh" "$tmp/repo/scripts/"
|
||||
cat > "$tmp/repo/divisions.json" <<'EOF'
|
||||
{
|
||||
"divisions": {
|
||||
"engineering": {}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
cat > "$tmp/repo/engineering/agent.md" <<'EOF'
|
||||
---
|
||||
name: Sample Agent
|
||||
description: Example agent
|
||||
---
|
||||
Instructions.
|
||||
EOF
|
||||
printf 'Gemini output\n' > "$tmp/repo/integrations/gemini-cli/agents/sample-agent.md"
|
||||
printf 'Qwen output\n' > "$tmp/repo/integrations/qwen/agents/sample-agent.md"
|
||||
# integrations/cursor is deliberately absent: cursor is the tool that fails.
|
||||
|
||||
status=0
|
||||
(cd "$tmp/home/project" && HOME="$tmp/home" PATH=/usr/bin:/bin \
|
||||
bash "$tmp/repo/scripts/install.sh" --no-interactive --no-convert \
|
||||
--tool gemini-cli,cursor,qwen > "$tmp/output" 2>&1) || status=$?
|
||||
|
||||
[[ "$status" -ne 0 ]] || fail "install exited 0 although cursor failed"
|
||||
[[ -f "$tmp/home/.gemini/agents/sample-agent.md" ]] || fail "gemini-cli, before the failure, did not install"
|
||||
[[ -f "$tmp/home/project/.qwen/agents/sample-agent.md" ]] \
|
||||
|| fail "qwen, after the failing cursor, was never installed"
|
||||
grep -q 'integrations/cursor missing' "$tmp/output" || fail "cursor's own error was not shown"
|
||||
grep -q 'Failed: cursor' "$tmp/output" || fail "the summary does not name the failed tool"
|
||||
grep -q 'Installed 2 of 3 tool(s)' "$tmp/output" || fail "the summary does not count the tools that installed"
|
||||
echo "PASS: a failing tool is reported and the tools after it still install"
|
||||
|
||||
# The clean path is unchanged: every tool installs and the install exits 0.
|
||||
mkdir -p "$tmp/repo/integrations/cursor/rules" "$tmp/home2/project"
|
||||
printf -- '---\ndescription: x\n---\nCursor output\n' > "$tmp/repo/integrations/cursor/rules/sample-agent.mdc"
|
||||
(cd "$tmp/home2/project" && HOME="$tmp/home2" PATH=/usr/bin:/bin \
|
||||
bash "$tmp/repo/scripts/install.sh" --no-interactive --no-convert \
|
||||
--tool gemini-cli,cursor,qwen > "$tmp/output" 2>&1) || fail "a clean install exited non-zero"
|
||||
grep -q 'Done! Installed 3 tool(s)' "$tmp/output" || fail "a clean install did not report all three tools"
|
||||
echo "PASS: a clean multi-tool install still exits 0"
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
scratch="$(mktemp -d)"
|
||||
trap 'rm -rf "$scratch"' EXIT
|
||||
mkdir -p "$scratch/repo/scripts" "$scratch/repo/engineering" "$scratch/repo/integrations" "$scratch/home"
|
||||
cp "$SCRIPT_DIR/install.sh" "$SCRIPT_DIR/lib.sh" "$scratch/repo/scripts/"
|
||||
cp "$SCRIPT_DIR/../divisions.json" "$scratch/repo/"
|
||||
export HOME="$scratch/home"
|
||||
for agent in first second; do
|
||||
printf '%s\n' '---' "name: $agent" 'description: Example agent' 'color: blue' '---' '# Example agent' > "$scratch/repo/engineering/$agent.md"
|
||||
done
|
||||
run_install() {
|
||||
bash "$scratch/repo/scripts/install.sh" --tool claude-code --division engineering \
|
||||
--no-convert --link --path "$scratch/dest" > "$scratch/result.log" 2>&1
|
||||
}
|
||||
mkdir -p "$scratch/dest"
|
||||
printf 'user content\n' > "$scratch/dest/first.md"
|
||||
run_install
|
||||
[[ ! -L "$scratch/dest/first.md" ]] && grep -qx 'user content' "$scratch/dest/first.md" || {
|
||||
echo 'FAIL: --link replaced a user-owned regular file' >&2; exit 1;
|
||||
}
|
||||
[[ -L "$scratch/dest/second.md" ]]
|
||||
grep -q 'Not installed: 1 file' "$scratch/result.log"
|
||||
for kind in foreign dangling directory; do
|
||||
rm -f "$scratch/dest/first.md" "$scratch/dest/second.md"
|
||||
target="$scratch/$kind"
|
||||
[[ "$kind" == foreign ]] && printf 'outside content\n' > "$target"
|
||||
[[ "$kind" == directory ]] && mkdir -p "$target"
|
||||
ln -s "$target" "$scratch/dest/first.md"
|
||||
run_install
|
||||
[[ "$(readlink "$scratch/dest/first.md")" == "$target" ]]
|
||||
[[ -L "$scratch/dest/second.md" ]]
|
||||
grep -q 'Not installed: 1 file' "$scratch/result.log"
|
||||
[[ "$kind" != foreign ]] || grep -qx 'outside content' "$target"
|
||||
[[ "$kind" != dangling ]] || [[ ! -e "$target" ]]
|
||||
[[ "$kind" != directory ]] || [[ ! -e "$target/first.md" ]]
|
||||
done
|
||||
rm -f "$scratch/dest/first.md"
|
||||
ln -s "$scratch/repo/engineering/old.md" "$scratch/dest/first.md"
|
||||
run_install
|
||||
[[ "$(readlink "$scratch/dest/first.md")" == "$scratch/repo/engineering/first.md" ]]
|
||||
run_install
|
||||
[[ "$(readlink "$scratch/dest/first.md")" == "$scratch/repo/engineering/first.md" ]]
|
||||
# A config directory itself may be a user's dotfiles link; protect leaf files.
|
||||
mv "$scratch/dest" "$scratch/dotfiles"
|
||||
ln -s "$scratch/dotfiles" "$scratch/dest"
|
||||
run_install
|
||||
[[ -L "$scratch/dest" && -L "$scratch/dest/second.md" ]]
|
||||
echo 'PASS: --link skips user files and foreign links, replaces its own links, and continues'
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
scratch="$(mktemp -d)"
|
||||
trap 'rm -rf "$scratch"' EXIT
|
||||
mkdir -p "$scratch/repo/scripts" "$scratch/repo/engineering" "$scratch/repo/integrations" "$scratch/home"
|
||||
cp "$SCRIPT_DIR/install.sh" "$SCRIPT_DIR/lib.sh" "$scratch/repo/scripts/"
|
||||
cp "$SCRIPT_DIR/../divisions.json" "$scratch/repo/"
|
||||
export HOME="$scratch/home"
|
||||
export CLAUDE_CONFIG_DIR="$HOME/.claude" COPILOT_AGENT_DIR="$HOME/.github/agents"
|
||||
for agent in first second; do
|
||||
printf '%s\n' '---' "name: $agent" 'description: Example agent' 'color: blue' '---' '# Example agent' > "$scratch/repo/engineering/$agent.md"
|
||||
done
|
||||
mkdir -p "$scratch/bin" "$scratch/tmp"
|
||||
cat > "$scratch/bin/cp" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
if [[ "$*" == *"/.claude/agents/"* && "${FAIL_WORKER:-yes}" == yes ]]; then
|
||||
echo 'fixture worker copy failed' >&2
|
||||
exit 47
|
||||
fi
|
||||
exec /bin/cp "$@"
|
||||
EOF
|
||||
chmod +x "$scratch/bin/cp"
|
||||
export PATH="$scratch/bin:$PATH" TMPDIR="$scratch/tmp"
|
||||
expected_status=0
|
||||
printf 'failure\n' | xargs -P 2 -I {} sh -c 'exit 47' || expected_status=$?
|
||||
status=0
|
||||
bash "$scratch/repo/scripts/install.sh" --tool claude-code,copilot --division engineering \
|
||||
--no-convert --parallel --jobs 2 > "$scratch/failure.log" 2>&1 || status=$?
|
||||
[[ "$status" == "$expected_status" && "$status" != 0 ]] || { cat "$scratch/failure.log"; echo "FAIL: worker failure status changed: $status"; exit 1; }
|
||||
grep -q 'fixture worker copy failed' "$scratch/failure.log" || {
|
||||
echo 'FAIL: buffered worker failure is hidden' >&2; exit 1;
|
||||
}
|
||||
[[ -f "$HOME/.github/agents/first.md" ]]
|
||||
! grep -q 'Done! Installed' "$scratch/failure.log"
|
||||
[[ -z "$(ls -A "$scratch/tmp")" ]] || { echo 'FAIL: worker output directory leaked'; exit 1; }
|
||||
export FAIL_WORKER=no
|
||||
bash "$scratch/repo/scripts/install.sh" --tool claude-code,copilot --division engineering \
|
||||
--no-convert --parallel --jobs 2 > "$scratch/success.log" 2>&1
|
||||
grep -q 'Done! Installed 2 tool(s)' "$scratch/success.log"
|
||||
[[ -f "$HOME/.claude/agents/first.md" ]]
|
||||
[[ -z "$(ls -A "$scratch/tmp")" ]]
|
||||
echo 'PASS: failed parallel worker logs are replayed, status is preserved, and buffers are removed'
|
||||
+23
-5
@@ -330,9 +330,9 @@ echo ""
|
||||
echo "parallel workers"
|
||||
|
||||
# Two tools that write the same filenames into one shared --path would silently
|
||||
# overwrite each other (claude-code and copilot both copy the raw source as
|
||||
# <division>-<slug>.md). The installer must refuse, not clobber. Both tools work
|
||||
# under --no-convert, which keeps this case cheap in CI.
|
||||
# overwrite each other (claude-code and copilot both copy the raw source under
|
||||
# its own name). The installer must refuse, not clobber. Both tools work under
|
||||
# --no-convert, which keeps this case cheap in CI.
|
||||
home="$(sandbox path-collision)"
|
||||
dest="$home/My [Agents]/dest dir"
|
||||
run_install "$home" --tool claude-code,copilot --no-convert --agent "$FIRST_ENG_SLUG" --path "$dest"
|
||||
@@ -340,8 +340,22 @@ assert_eq 1 "$RUN_STATUS" "two tools that write the same filenames into one --pa
|
||||
assert_eq 1 "$(printf '%s' "$RUN_OUT" | grep -c 'overwrite')" "the refusal explains the collision"
|
||||
assert_eq 0 "$(count_md "$dest")" "a refused install writes nothing"
|
||||
|
||||
# A source file's own name is not always <division>-<slug>.md: 73 agents are
|
||||
# named <slug>.md, which is what the converted .md tools write. claude-code and
|
||||
# qwen with one of those used to report two successful installs while qwen's
|
||||
# file replaced Claude Code's. The refusal comes before any conversion, so this
|
||||
# stays cheap too.
|
||||
slug_named="$(for f in "$REPO_ROOT"/game-development/*.md; do
|
||||
is_agent_file "$f" && [[ "$(basename "$f" .md)" == "$(agent_slug "$f")" ]] && { agent_slug "$f"; break; }
|
||||
done)"
|
||||
home="$(sandbox path-collision-slug-named)"
|
||||
dest="$home/dest"
|
||||
run_install "$home" --tool claude-code,qwen --agent "$slug_named" --path "$dest"
|
||||
assert_eq 1 "$RUN_STATUS" "claude-code and qwen share <slug>.md names, so one --path is refused ($slug_named)"
|
||||
assert_eq 0 "$(count_md "$dest")" "the refused claude-code+qwen install writes nothing"
|
||||
|
||||
# The propagation cases below therefore use a NON-colliding pair: claude-code
|
||||
# writes <division>-<slug>.md and codex writes <slug>.toml, so BOTH outputs must
|
||||
# writes .md and codex writes <slug>.toml, so BOTH outputs must
|
||||
# survive in the shared --path — which is a stronger check than one tool's count
|
||||
# alone (a count of 1 cannot tell "two wrote, one clobbered" from "one wrote").
|
||||
# codex has no committed output (integrations/ is generated and gitignored), so
|
||||
@@ -361,7 +375,11 @@ dest="$home/My [Agents]/dest dir"
|
||||
list="$home/my agents list.txt"
|
||||
{ echo "# one agent, listed in a file whose own path has spaces"; echo "$FIRST_ENG_SLUG"; } > "$list"
|
||||
run_install "$home" --tool claude-code,codex --parallel --jobs 1 --agents-file "$list" --path "$dest"
|
||||
assert_eq 0 "$RUN_STATUS" "--parallel with a spaced/globbed --path exits 0"
|
||||
# This used to pass for the wrong reason: the workers get the spaced path split
|
||||
# into words, reject the stray words as unknown options, and usage() exited 0,
|
||||
# so every worker "succeeded" having installed nothing (the count below).
|
||||
# Unknown options exit 1 now, so the exit code tells the truth until #755 lands.
|
||||
xfail_eq 0 "$RUN_STATUS" "--parallel with a spaced/globbed --path exits 0" "PR #755"
|
||||
xfail_eq 1 "$(count_md "$dest")" \
|
||||
"--parallel installs exactly the one selected agent (spaced --path + --agents-file)" "PR #755"
|
||||
|
||||
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# Code fences that GitHub renders differently from what the author meant must
|
||||
# fail lint, and the fix the error message suggests must pass it.
|
||||
#
|
||||
# Markdown has no nesting at equal fence length. Inside a ```markdown template,
|
||||
# a ```bash line is text, and the example's closing ``` ends the template, so
|
||||
# the rest of it renders as headings and prose. Seven roster agents shipped
|
||||
# that way, three of them with the last block left open to the end of the file.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
FIXTURE="$(mktemp -d "${TMPDIR:-/tmp}/agency-lint-fences.XXXXXX")"
|
||||
trap 'rm -rf "$FIXTURE"' EXIT
|
||||
|
||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||
|
||||
frontmatter() {
|
||||
cat <<'EOF'
|
||||
---
|
||||
name: Fence Fixture
|
||||
description: Fixture agent for the code-fence lint rules
|
||||
color: blue
|
||||
---
|
||||
## Identity
|
||||
Enough words to clear the short-body warning, repeated so the linter has a
|
||||
body to read: this fixture exists to check how fenced code blocks are parsed.
|
||||
## Core Mission
|
||||
Show a template that contains a code example.
|
||||
## Critical Rules
|
||||
Keep fences balanced.
|
||||
EOF
|
||||
}
|
||||
|
||||
# 1. ```bash inside ```markdown: the inner fence does not nest.
|
||||
{ frontmatter; cat <<'EOF'
|
||||
### Template
|
||||
```markdown
|
||||
# Report
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
## Findings
|
||||
```
|
||||
EOF
|
||||
} > "$FIXTURE/nested.md"
|
||||
if bash "$SCRIPT_DIR/lint-agents.sh" "$FIXTURE/nested.md" > "$FIXTURE/nested.log" 2>&1; then
|
||||
fail "linter accepted a \`\`\`bash fence nested in a \`\`\`markdown block of the same length"
|
||||
fi
|
||||
grep -Fq "nested.md:16: '\`\`\`bash' inside the block opened at line 14 does not nest" "$FIXTURE/nested.log" \
|
||||
|| { cat "$FIXTURE/nested.log" >&2; fail "nested-fence error is missing or names the wrong lines"; }
|
||||
|
||||
# 2. A block left open to the end of the file.
|
||||
{ frontmatter; printf '%s\n' '### Layout' '```' 'css/' 'js/'; } > "$FIXTURE/unclosed.md"
|
||||
if bash "$SCRIPT_DIR/lint-agents.sh" "$FIXTURE/unclosed.md" > "$FIXTURE/unclosed.log" 2>&1; then
|
||||
fail "linter accepted a code block that is never closed"
|
||||
fi
|
||||
grep -Fq "unclosed.md:14: code block is never closed" "$FIXTURE/unclosed.log" \
|
||||
|| { cat "$FIXTURE/unclosed.log" >&2; fail "unclosed-fence error is missing or names the wrong line"; }
|
||||
|
||||
# 3. The fix the message suggests — a longer outer fence — passes, and so do
|
||||
# tilde fences and a shorter run inside a longer block.
|
||||
{ frontmatter; cat <<'EOF'
|
||||
### Template
|
||||
````markdown
|
||||
# Report
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
## Findings
|
||||
````
|
||||
~~~text
|
||||
```python is just text in here
|
||||
~~~
|
||||
EOF
|
||||
} > "$FIXTURE/valid.md"
|
||||
bash "$SCRIPT_DIR/lint-agents.sh" "$FIXTURE/valid.md" > "$FIXTURE/valid.log" 2>&1 \
|
||||
|| { cat "$FIXTURE/valid.log" >&2; fail "linter rejected correctly nested fences"; }
|
||||
|
||||
# 4. The helper itself, which the OpenClaw split in convert.sh shares: only a
|
||||
# bare run of the same character, at least as long, closes a block.
|
||||
. "$SCRIPT_DIR/lib.sh"
|
||||
fence_closes_p '```python' '`' 3 0 && fail "fence_closes_p treated '\`\`\`python' as a closing fence"
|
||||
fence_closes_p '```' '`' 3 0 || fail "fence_closes_p rejected a bare closing fence"
|
||||
fence_closes_p '```` ' '`' 3 0 || fail "fence_closes_p rejected a longer closing fence with trailing spaces"
|
||||
fence_closes_p '```' '`' 4 0 && fail "fence_closes_p let a shorter run close a longer fence"
|
||||
|
||||
echo "PASS: nested and unclosed fences are rejected; correctly nested fences pass"
|
||||
@@ -233,43 +233,70 @@ class DependencyScanner:
|
||||
"CVE-2023-XXXXX": "Not exploitable in our configuration — validated by AppSec team 2024-01-15",
|
||||
}
|
||||
|
||||
def scan_npm(self, project_path: Path) -> list[VulnFinding]:
|
||||
"""Scan Node.js dependencies using npm audit."""
|
||||
@staticmethod
|
||||
def audit_json(command: list[str], project_path: Path) -> dict:
|
||||
"""Exit 1 may mean findings; tool errors are never a clean scan."""
|
||||
result = subprocess.run(
|
||||
["npm", "audit", "--json", "--production"],
|
||||
cwd=project_path, capture_output=True, text=True
|
||||
command, cwd=project_path, capture_output=True, text=True
|
||||
)
|
||||
findings = []
|
||||
if result.stdout:
|
||||
try:
|
||||
audit = json.loads(result.stdout)
|
||||
for vuln_id, vuln in audit.get("vulnerabilities", {}).items():
|
||||
findings.append(VulnFinding(
|
||||
package=vuln_id,
|
||||
version=vuln.get("range", "unknown"),
|
||||
severity=Severity(vuln.get("severity", "low")),
|
||||
cve=vuln.get("via", [{}])[0].get("url", "N/A") if vuln.get("via") else "N/A",
|
||||
fixed_version=vuln.get("fixAvailable", {}).get("version", "N/A")
|
||||
if isinstance(vuln.get("fixAvailable"), dict) else "N/A",
|
||||
description=vuln.get("via", [{}])[0].get("title", "")
|
||||
if isinstance(vuln.get("via", [None])[0], dict) else str(vuln.get("via", "")),
|
||||
))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise RuntimeError(f"{command[0]} did not produce valid JSON") from exc
|
||||
if result.returncode not in (0, 1) or not isinstance(audit, dict) or audit.get("error"):
|
||||
raise RuntimeError(f"{command[0]} failed (exit {result.returncode})")
|
||||
return audit
|
||||
|
||||
def scan_npm(self, project_path: Path) -> list[VulnFinding]:
|
||||
"""Scan Node.js dependencies using npm audit's current JSON report."""
|
||||
audit = self.audit_json(["npm", "audit", "--json", "--omit=dev"], project_path)
|
||||
vulnerabilities = audit.get("vulnerabilities")
|
||||
if not isinstance(vulnerabilities, dict):
|
||||
raise RuntimeError("npm audit report is missing vulnerabilities")
|
||||
findings = []
|
||||
for package, vuln in vulnerabilities.items():
|
||||
# via contains advisory objects OR names of indirect dependencies.
|
||||
via = vuln.get("via", [])
|
||||
advisories = [item for item in via if isinstance(item, dict)]
|
||||
fix = vuln.get("fixAvailable")
|
||||
severity = vuln.get("severity")
|
||||
if severity not in {item.value for item in Severity} | {"info"}:
|
||||
raise RuntimeError(f"npm audit returned unknown severity for {package}")
|
||||
findings.append(VulnFinding(
|
||||
package=package,
|
||||
version=vuln.get("range", "unknown"),
|
||||
severity=Severity.LOW if severity == "info" else Severity(severity),
|
||||
cve=advisories[0].get("url", "N/A") if advisories else "N/A",
|
||||
fixed_version=(fix.get("version", "N/A") if isinstance(fix, dict)
|
||||
else "available" if fix is True else "N/A"),
|
||||
description="; ".join(item.get("title", "") for item in advisories)
|
||||
or "Indirect dependency vulnerability: " + ", ".join(map(str, via)),
|
||||
))
|
||||
return findings
|
||||
|
||||
def scan_python(self, project_path: Path) -> list[VulnFinding]:
|
||||
"""Scan Python dependencies using pip-audit."""
|
||||
result = subprocess.run(
|
||||
["pip-audit", "--format=json", "--desc"],
|
||||
cwd=project_path, capture_output=True, text=True
|
||||
)
|
||||
"""Audit requirements, or the active environment with the project installed."""
|
||||
command = ["pip-audit", "--format=json", "--desc"]
|
||||
if (project_path / "requirements.txt").exists():
|
||||
command.extend(["-r", "requirements.txt"])
|
||||
audit = self.audit_json(command, project_path)
|
||||
dependencies = audit.get("dependencies")
|
||||
if not isinstance(dependencies, list):
|
||||
raise RuntimeError("pip-audit report is missing dependencies")
|
||||
findings = []
|
||||
if result.stdout:
|
||||
for vuln in json.loads(result.stdout):
|
||||
for dependency in dependencies:
|
||||
if dependency.get("skip_reason"):
|
||||
raise RuntimeError(f"pip-audit skipped {dependency['name']}")
|
||||
vulnerabilities = dependency.get("vulns")
|
||||
if not isinstance(vulnerabilities, list):
|
||||
raise RuntimeError("pip-audit dependency is missing vulns")
|
||||
for vuln in vulnerabilities:
|
||||
findings.append(VulnFinding(
|
||||
package=vuln["name"],
|
||||
version=vuln["version"],
|
||||
severity=Severity.HIGH, # pip-audit doesn't always provide severity
|
||||
cve=vuln.get("id", "N/A"),
|
||||
fixed_version=vuln.get("fix_versions", ["N/A"])[0],
|
||||
package=dependency["name"],
|
||||
version=dependency["version"],
|
||||
severity=Severity.HIGH, # Conservative local policy, not tool-provided severity
|
||||
cve=vuln["id"],
|
||||
fixed_version=", ".join(vuln.get("fix_versions", [])) or "N/A",
|
||||
description=vuln.get("description", ""),
|
||||
))
|
||||
return findings
|
||||
@@ -307,12 +334,17 @@ def main():
|
||||
scanner = DependencyScanner()
|
||||
project = Path(".")
|
||||
|
||||
# Detect project type and scan
|
||||
# Detect project type and scan. An unavailable scanner, malformed report,
|
||||
# unsupported report shape, or skipped dependency leaves coverage incomplete.
|
||||
findings = []
|
||||
if (project / "package.json").exists():
|
||||
findings.extend(scanner.scan_npm(project))
|
||||
if (project / "requirements.txt").exists() or (project / "pyproject.toml").exists():
|
||||
findings.extend(scanner.scan_python(project))
|
||||
try:
|
||||
if (project / "package.json").exists():
|
||||
findings.extend(scanner.scan_npm(project))
|
||||
if (project / "requirements.txt").exists() or (project / "pyproject.toml").exists():
|
||||
findings.extend(scanner.scan_python(project))
|
||||
except (OSError, RuntimeError, KeyError, TypeError, ValueError) as exc:
|
||||
print(f"SCAN INCOMPLETE: {exc}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
# Enforce policy
|
||||
passed, violations = scanner.enforce_policy(findings)
|
||||
@@ -331,6 +363,15 @@ if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
For a `pyproject.toml` project, install its locked runtime dependencies into an
|
||||
isolated environment before invoking this wrapper; `pip-audit` without `-r`
|
||||
audits the active environment. Exit 2 means the gate could not complete and must
|
||||
block promotion until the scanner/report problem is resolved. Exercise fixtures
|
||||
for clean reports, advisory findings, indirect npm `via` strings, empty Python
|
||||
`fix_versions`, scanner failures, invalid JSON, and skipped dependencies. See the
|
||||
[pip-audit JSON format and exit codes](https://github.com/pypa/pip-audit#usage)
|
||||
and [npm audit report behavior](https://docs.npmjs.com/cli/v11/commands/npm-audit).
|
||||
|
||||
### Threat Model Template (STRIDE)
|
||||
```markdown
|
||||
# Threat Model: [Feature/System Name]
|
||||
|
||||
@@ -150,6 +150,12 @@ contract SecureLending {
|
||||
AggregatorV3Interface immutable priceFeed;
|
||||
uint256 constant MAX_ORACLE_STALENESS = 1 hours;
|
||||
|
||||
constructor(address feed) {
|
||||
priceFeed = AggregatorV3Interface(feed);
|
||||
}
|
||||
|
||||
// amount uses the collateral token's base units. With a USD/token feed,
|
||||
// the result is USD scaled by the collateral token's decimal count.
|
||||
function getCollateralValue(uint256 amount) public view returns (uint256) {
|
||||
(
|
||||
uint80 roundId,
|
||||
@@ -164,11 +170,24 @@ contract SecureLending {
|
||||
require(updatedAt > block.timestamp - MAX_ORACLE_STALENESS, "Stale price");
|
||||
require(answeredInRound >= roundId, "Incomplete round");
|
||||
|
||||
return (amount * uint256(price)) / priceFeed.decimals();
|
||||
uint8 feedDecimals = priceFeed.decimals();
|
||||
require(feedDecimals <= 77, "Unsupported feed decimals");
|
||||
// decimals() is the number of decimal places, not the scale factor.
|
||||
return (amount * uint256(price)) / (10 ** uint256(feedDecimals));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Before comparing collateral value with debt, normalize both to the same unit.
|
||||
For one 18-decimal token (`amount = 1e18`) priced at $2,000 by an 8-decimal
|
||||
feed (`price = 2000e8`), this function returns `2000e18`, not `25000000000e18`.
|
||||
A 6-decimal token produces `2000e6`; converting that to a debt asset's base
|
||||
units is a separate step. Test both scales and a zero-decimal feed, which must
|
||||
not divide by zero. Solidity's checked multiplication still reverts on extreme
|
||||
products; production code should use a reviewed full-precision `mulDiv` if its
|
||||
supported input range can overflow. Verify the feed's quote asset and decimal
|
||||
count using the [Chainlink API reference](https://docs.chain.link/data-feeds/api-reference).
|
||||
|
||||
### Access Control Audit Checklist
|
||||
```markdown
|
||||
# Access Control Audit Checklist
|
||||
|
||||
@@ -297,6 +297,50 @@ spec:
|
||||
- protocol: TCP
|
||||
port: 5432
|
||||
|
||||
---
|
||||
# Sender egress must also allow frontend → backend API under default-deny
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-frontend-api-egress
|
||||
namespace: production
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app: frontend
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app: backend-api
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
|
||||
---
|
||||
# Sender egress must also allow backend API → database
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-api-database-egress
|
||||
namespace: production
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app: backend-api
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5432
|
||||
|
||||
---
|
||||
# Allow DNS egress for all pods (required for service discovery)
|
||||
apiVersion: networking.k8s.io/v1
|
||||
@@ -323,6 +367,15 @@ spec:
|
||||
port: 53
|
||||
```
|
||||
|
||||
Both the sender's egress and the receiver's ingress must permit a connection.
|
||||
The selectors above target pods in `production`; they do not grant the same
|
||||
labels in other namespaces access. Use a NetworkPolicy-enforcing CNI and verify
|
||||
frontend → API:8080 and API → database:5432 succeed, while frontend → database,
|
||||
API → database:5433, and API → arbitrary external destinations remain blocked.
|
||||
DNS labels must match the cluster's actual DNS pods; NodeLocal DNS needs a
|
||||
cluster-specific policy. Reply traffic for an allowed connection is implicit.
|
||||
See [Kubernetes NetworkPolicy semantics](https://kubernetes.io/docs/concepts/services-networking/network-policies/).
|
||||
|
||||
### CI/CD Pipeline Security (GitHub Actions with OIDC)
|
||||
```yaml
|
||||
# Secure deployment pipeline — no long-lived credentials
|
||||
|
||||
@@ -206,14 +206,27 @@ Write-Host "[!] NEXT: Copy $outDir to analysis workstation — do NOT analyze on
|
||||
```
|
||||
|
||||
### Linux Forensic Triage Script
|
||||
|
||||
A suspected host may already be hostile: use trusted collection tools and an
|
||||
approved evidence destination. This local triage is not a substitute for a
|
||||
forensic image. Restrict the collection directory to the collector, preserve it
|
||||
for handoff, and never write through a pre-existing path supplied by another user.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Linux Incident Response Triage Collection
|
||||
# Run as root on suspected compromised system
|
||||
|
||||
TIMESTAMP=$(date -u +"%Y%m%d-%H%M%S")
|
||||
OUTDIR="/tmp/ir-triage-${HOSTNAME}-${TIMESTAMP}"
|
||||
mkdir -p "$OUTDIR"
|
||||
# Evidence can contain credentials. Create a private directory atomically;
|
||||
# never reuse a predictable path in /tmp or trust an inherited TMPDIR.
|
||||
umask 077
|
||||
OUTDIR=$(mktemp -d /tmp/ir-triage.XXXXXXXXXX) || {
|
||||
echo "[!] Unable to create private evidence directory" >&2
|
||||
exit 1
|
||||
}
|
||||
readonly OUTDIR
|
||||
# Preserve the directory for handoff; do not delete evidence in an EXIT trap.
|
||||
|
||||
echo "[*] Starting Linux IR triage at ${TIMESTAMP} UTC"
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
|
||||
### Static Key → Dynamic, Short-Lived Credential
|
||||
|
||||
```hcl
|
||||
```bash
|
||||
# BEFORE: a long-lived static DB password in an env var — one leak = full, permanent access.
|
||||
# DATABASE_URL=postgres://app:sup3rs3cret@db.internal:5432/app # never rotated, everywhere
|
||||
|
||||
@@ -95,11 +95,22 @@ jobs:
|
||||
vault write database/roles/app \
|
||||
db_name=appdb \
|
||||
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
|
||||
GRANT SELECT, INSERT, UPDATE ON app.* TO \"{{name}}\";" \
|
||||
GRANT USAGE ON SCHEMA app TO \"{{name}}\"; \
|
||||
GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA app TO \"{{name}}\";" \
|
||||
default_ttl="15m" max_ttl="1h"
|
||||
# The app fetches a fresh, least-privilege credential per session; a leaked one is dead in minutes.
|
||||
```
|
||||
|
||||
This PostgreSQL example assumes the dedicated `app` schema contains only tables
|
||||
this workload may access, and the Vault database connection role can create roles
|
||||
and grant those privileges. `app.*` is not PostgreSQL GRANT syntax; schema `USAGE`
|
||||
and table privileges are separate. The grants cover existing tables only. Reissue
|
||||
credentials after migrations or maintain a reviewed grant strategy for future
|
||||
tables. Sequence-backed inserts need narrowly scoped sequence `USAGE` as well.
|
||||
Verify a leased role can SELECT/INSERT/UPDATE an allowed table but cannot DELETE,
|
||||
CREATE a table, or access another schema. See [PostgreSQL GRANT](https://www.postgresql.org/docs/current/sql-grant.html)
|
||||
and [Vault's database secrets tutorial](https://developer.hashicorp.com/vault/tutorials/db-credentials/database-secrets).
|
||||
|
||||
### Leak-Response Runbook (the clock started at commit)
|
||||
|
||||
```markdown
|
||||
|
||||
@@ -122,7 +122,6 @@ fields:
|
||||
|
||||
### Compiled to Splunk SPL
|
||||
```spl
|
||||
| Suspicious PowerShell Encoded Command — compiled from Sigma rule
|
||||
index=windows sourcetype=WinEventLog:Sysmon EventCode=1
|
||||
(ParentImage="*\\cmd.exe" OR ParentImage="*\\wscript.exe"
|
||||
OR ParentImage="*\\cscript.exe" OR ParentImage="*\\mshta.exe"
|
||||
@@ -135,7 +134,6 @@ index=windows sourcetype=WinEventLog:Sysmon EventCode=1
|
||||
ParentImage LIKE "%mshta.exe", 85,
|
||||
1=1, 70
|
||||
)
|
||||
| where NOT match(CommandLine, "(?i)(SCCM|ConfigMgr|Intune)")
|
||||
| table _time Computer User ParentImage Image CommandLine risk_score
|
||||
| sort - risk_score
|
||||
```
|
||||
@@ -152,9 +150,6 @@ DeviceProcessEvents
|
||||
| where ProcessCommandLine has_any (
|
||||
"-enc ", "-EncodedCommand", "-ec ", "FromBase64String"
|
||||
)
|
||||
// Exclude known legitimate automation
|
||||
| where ProcessCommandLine !contains "SCCM"
|
||||
and ProcessCommandLine !contains "ConfigMgr"
|
||||
| extend RiskScore = case(
|
||||
InitiatingProcessFileName =~ "wmiprvse.exe", 90,
|
||||
InitiatingProcessFileName =~ "mshta.exe", 85,
|
||||
@@ -165,6 +160,24 @@ DeviceProcessEvents
|
||||
| sort by RiskScore desc
|
||||
```
|
||||
|
||||
### Validate Exceptions Against Attacker-Controlled Input
|
||||
|
||||
Keep these example queries free of command-line substring exclusions. An attacker
|
||||
can append `# SCCM`, `# ConfigMgr`, or `# Intune` to a suspicious PowerShell command;
|
||||
that string does not establish that a trusted deployment system launched it.
|
||||
Investigate the alert using host enrollment, expected service identity, verified
|
||||
parent binary path/signature, and the deployment job's audit trail. If an exception
|
||||
is approved, scope it to that evidence, record an owner and expiry, and test it
|
||||
against benign automation and malicious lookalikes. A parent executable name alone
|
||||
is not sufficient either. Keep the Sigma and SIEM implementations equivalent and
|
||||
label any environment-specific exception explicitly.
|
||||
|
||||
Replay the same positive process event with all four command lines: the original,
|
||||
then the original plus each of those three comments. All four must alert. Include
|
||||
a negative non-PowerShell event to prove the rule is not matching everything.
|
||||
Use [Sigma rule testing and tuning guidance](https://sigmahq.io/docs/basics/rules.html)
|
||||
and the target SIEM's own query test facilities before deployment.
|
||||
|
||||
### MITRE ATT&CK Coverage Assessment Template
|
||||
```markdown
|
||||
# MITRE ATT&CK Detection Coverage Report
|
||||
|
||||
@@ -95,7 +95,7 @@ You are a **Developer Advocate**, the trusted engineer who lives at the intersec
|
||||
```
|
||||
|
||||
### Viral Tutorial Structure
|
||||
```markdown
|
||||
````markdown
|
||||
# Build a [Real Thing] with [Your Platform] in [Honest Time]
|
||||
|
||||
**Live demo**: [link] | **Full source**: [GitHub link]
|
||||
@@ -144,7 +144,7 @@ Ready to go further?
|
||||
- → [Add authentication to your dashboard](link)
|
||||
- → [Deploy to production on Vercel](link)
|
||||
- → [Explore the full API reference](link)
|
||||
```
|
||||
````
|
||||
|
||||
### Conference Talk Proposal Template
|
||||
```markdown
|
||||
@@ -179,7 +179,7 @@ Why this speaker: relevant experience and credibility signal.]
|
||||
```
|
||||
|
||||
### GitHub Issue Response Templates
|
||||
```markdown
|
||||
````markdown
|
||||
<!-- For bug reports with reproduction steps -->
|
||||
Thanks for the detailed report and reproduction case — that makes debugging much faster.
|
||||
|
||||
@@ -206,7 +206,7 @@ likelihood/priority].
|
||||
|
||||
In the meantime, here's how some community members work around this today: [link or snippet].
|
||||
|
||||
```
|
||||
````
|
||||
|
||||
### Developer Survey Design
|
||||
```javascript
|
||||
|
||||
@@ -62,7 +62,7 @@ You operate under a strict **Zero Code Execution** guardrail: you design the blu
|
||||
|
||||
### The 5-Part Standard Implementation Plan Schema (`.md`)
|
||||
|
||||
```markdown
|
||||
````markdown
|
||||
# 🏛️ [Project/Module Name] — Architectural Blueprint & Governance Plan
|
||||
|
||||
## 1. 🎓 Conceptual Masterclass: Philosophy, First Principles & Landscape
|
||||
@@ -99,7 +99,7 @@ graph TD
|
||||
## 5. 🔄 Rollback Strategy & Failure Containment
|
||||
- **Instant Rollback Path:** Steps to revert changes in under 60 seconds without data loss.
|
||||
- **Circuit Breakers:** Degradation mode if downstream dependencies fail.
|
||||
```
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -110,6 +110,12 @@ await server.connect(transport);
|
||||
### Python MCP Server
|
||||
|
||||
```python
|
||||
# MCP Python SDK 1.x: pip install 'mcp>=1,<2' httpx
|
||||
# SDK 2.x uses a different server API; this example targets FastMCP.
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import httpx
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from pydantic import Field
|
||||
|
||||
@@ -133,7 +139,13 @@ async def search_issues(
|
||||
headers={"Authorization": f"token {os.environ['GITHUB_TOKEN']}"},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
issues = [{"number": i["number"], "title": i["title"], "author": i["user"]["login"], "labels": [l["name"] for l in i["labels"]]} for i in resp.json()]
|
||||
# GitHub's repository issues endpoint also returns pull requests.
|
||||
# Keep this tool's issue-only contract; limit bounds the listing page.
|
||||
issues = [
|
||||
{"number": i["number"], "title": i["title"],
|
||||
"author": i["user"]["login"], "labels": [l["name"] for l in i["labels"]]}
|
||||
for i in resp.json() if "pull_request" not in i
|
||||
]
|
||||
return json.dumps(issues, indent=2)
|
||||
|
||||
@mcp.resource("repo://readme")
|
||||
|
||||
@@ -109,26 +109,35 @@ import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
def compute_psi(expected: pd.Series, actual: pd.Series, bins: int = 10) -> float:
|
||||
"""Baseline-quantile PSI; include out-of-range observations in tail bins.
|
||||
|
||||
<0.10: little shift; 0.10–0.25: investigate; >=0.25: significant shift.
|
||||
These are monitoring heuristics, not a model validity certificate.
|
||||
"""
|
||||
Compute Population Stability Index between two distributions.
|
||||
|
||||
Interpretation:
|
||||
< 0.10 → No significant shift (green)
|
||||
0.10–0.25 → Moderate shift, investigation recommended (amber)
|
||||
>= 0.25 → Significant shift, action required (red)
|
||||
"""
|
||||
breakpoints = np.linspace(0, 100, bins + 1)
|
||||
expected_pcts = np.percentile(expected.dropna(), breakpoints)
|
||||
if not isinstance(bins, int) or isinstance(bins, bool) or bins < 2:
|
||||
raise ValueError("bins must be an integer >= 2")
|
||||
baseline = expected.dropna().to_numpy(dtype=float)
|
||||
observed = actual.dropna().to_numpy(dtype=float)
|
||||
if not len(baseline) or not len(observed):
|
||||
raise ValueError("PSI requires nonempty baseline and observed samples")
|
||||
if not np.isfinite(baseline).all() or not np.isfinite(observed).all():
|
||||
raise ValueError("PSI samples must be finite")
|
||||
|
||||
expected_counts = np.histogram(expected, bins=expected_pcts)[0]
|
||||
actual_counts = np.histogram(actual, bins=expected_pcts)[0]
|
||||
|
||||
# Laplace smoothing to avoid division by zero
|
||||
exp_pct = (expected_counts + 1) / (expected_counts.sum() + bins)
|
||||
act_pct = (actual_counts + 1) / (actual_counts.sum() + bins)
|
||||
|
||||
psi = np.sum((act_pct - exp_pct) * np.log(act_pct / exp_pct))
|
||||
return round(psi, 6)
|
||||
# Unique interior quantiles handle repeated/constant baseline values.
|
||||
interior = np.unique(np.percentile(baseline, np.linspace(0, 100, bins + 1)[1:-1]))
|
||||
if np.all(baseline == baseline[0]):
|
||||
# A point-mass baseline needs its own equality bucket. Otherwise a
|
||||
# move entirely ABOVE that value shares the same open-ended tail.
|
||||
value = baseline[0]
|
||||
interior = np.array([value, np.nextafter(value, np.inf)])
|
||||
edges = np.unique(np.concatenate(([-np.inf], interior, [np.inf])))
|
||||
expected_counts = np.histogram(baseline, bins=edges)[0]
|
||||
actual_counts = np.histogram(observed, bins=edges)[0]
|
||||
bucket_count = len(edges) - 1
|
||||
# Normalize smoothing with the actual number of nonduplicate buckets.
|
||||
exp_pct = (expected_counts + 1) / (len(baseline) + bucket_count)
|
||||
act_pct = (actual_counts + 1) / (len(observed) + bucket_count)
|
||||
return round(float(np.sum((act_pct - exp_pct) * np.log(act_pct / exp_pct))), 6)
|
||||
```
|
||||
|
||||
### Discrimination Metrics (Gini & KS)
|
||||
|
||||
@@ -229,7 +229,7 @@ Every time I make an assumption that I cannot verify from the available code and
|
||||
|
||||
Every workflow spec follows this structure:
|
||||
|
||||
```markdown
|
||||
````markdown
|
||||
# WORKFLOW: [Name]
|
||||
**Version**: 0.1
|
||||
**Date**: YYYY-MM-DD
|
||||
@@ -392,7 +392,7 @@ Every workflow spec follows this structure:
|
||||
| Date | Finding | Action taken |
|
||||
|---|---|---|
|
||||
| YYYY-MM-DD | Initial spec created | — |
|
||||
```
|
||||
````
|
||||
|
||||
### Discovery Audit Checklist
|
||||
|
||||
|
||||
@@ -93,7 +93,12 @@ You are **AccessibilityAuditor**, an expert accessibility specialist who ensures
|
||||
- Moderate: [Count] — Causes difficulty but has workarounds
|
||||
- Minor: [Count] — Annoyances that reduce usability
|
||||
|
||||
**WCAG Conformance**: DOES NOT CONFORM / PARTIALLY CONFORMS / CONFORMS
|
||||
**Audit Scope**: [URLs, complete processes, UI states, date, and technologies tested]
|
||||
**Criteria Results**: [PASS / FAIL / NOT TESTED / NOT APPLICABLE, with evidence]
|
||||
**WCAG Conformance**: [DOES NOT CONFORM if any in-scope A/AA criterion fails;
|
||||
NOT DETERMINED if required tests are incomplete; CONFORMS only after evaluating
|
||||
all applicable A/AA criteria for full pages and complete processes]
|
||||
**Untested Scope**: [Pages, states, or assistive technology combinations not assessed]
|
||||
**Assistive Technology Compatibility**: FAIL / PARTIAL / PASS
|
||||
|
||||
## 🚨 Issues Found
|
||||
@@ -218,8 +223,9 @@ You are **AccessibilityAuditor**, an expert accessibility specialist who ensures
|
||||
|
||||
### Step 1: Automated Baseline Scan
|
||||
```bash
|
||||
# Run axe-core against all pages
|
||||
npx @axe-core/cli http://localhost:8000 --tags wcag2a,wcag2aa,wcag22aa
|
||||
# Automated subset of WCAG 2.2 A/AA: include criteria introduced in 2.1.
|
||||
# Scan each in-scope URL and relevant UI state; one URL is not the whole site.
|
||||
npx @axe-core/cli http://localhost:8000 --tags wcag2a,wcag2aa,wcag21a,wcag21aa,wcag22aa
|
||||
|
||||
# Run Lighthouse accessibility audit
|
||||
npx lighthouse http://localhost:8000 --only-categories=accessibility --output=json
|
||||
@@ -229,6 +235,11 @@ npx lighthouse http://localhost:8000 --only-categories=accessibility --output=js
|
||||
# Identify all custom interactive components for manual testing
|
||||
```
|
||||
|
||||
Automated results cover only the rules the tool can evaluate, even with every
|
||||
WCAG tag selected. Use the [axe-core tag inventory](https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#axe-core-tags)
|
||||
and the [WCAG conformance requirements](https://www.w3.org/TR/WCAG22/#conformance-reqs)
|
||||
to document scope; a clean scan or a sampled page does not establish site conformance.
|
||||
|
||||
### Step 2: Manual Assistive Technology Testing
|
||||
- Navigate every user journey with keyboard only — no mouse
|
||||
- Complete all critical flows with a screen reader (VoiceOver on macOS, NVDA on Windows)
|
||||
|
||||
@@ -58,16 +58,20 @@ You are **API Tester**, an expert API testing specialist who focuses on comprehe
|
||||
## 📋 Your Technical Deliverables
|
||||
|
||||
### Comprehensive API Test Suite Example
|
||||
```javascript
|
||||
```typescript
|
||||
// Save as tests/api.spec.ts. Run only against an authorized test environment.
|
||||
// Advanced API test automation with security and performance
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { performance } from 'perf_hooks';
|
||||
|
||||
describe('User API Comprehensive Testing', () => {
|
||||
test.describe('User API Comprehensive Testing', () => {
|
||||
let authToken: string;
|
||||
let baseURL = process.env.API_BASE_URL;
|
||||
const baseURL = process.env.API_BASE_URL;
|
||||
if (!baseURL || !process.env.TEST_USER_PASSWORD) {
|
||||
throw new Error('API_BASE_URL and TEST_USER_PASSWORD are required');
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
test.beforeAll(async () => {
|
||||
// Authenticate and get token
|
||||
const response = await fetch(`${baseURL}/auth/login`, {
|
||||
method: 'POST',
|
||||
@@ -77,11 +81,14 @@ describe('User API Comprehensive Testing', () => {
|
||||
password: process.env.TEST_USER_PASSWORD
|
||||
})
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
const data = await response.json();
|
||||
expect(typeof data.token).toBe('string');
|
||||
expect(data.token.length).toBeGreaterThan(0);
|
||||
authToken = data.token;
|
||||
});
|
||||
|
||||
describe('Functional Testing', () => {
|
||||
test.describe('Functional Testing', () => {
|
||||
test('should create user with valid data', async () => {
|
||||
const userData = {
|
||||
name: 'Test User',
|
||||
@@ -127,7 +134,7 @@ describe('User API Comprehensive Testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Security Testing', () => {
|
||||
test.describe('Security Testing', () => {
|
||||
test('should reject requests without authentication', async () => {
|
||||
const response = await fetch(`${baseURL}/users`, {
|
||||
method: 'GET'
|
||||
@@ -137,7 +144,7 @@ describe('User API Comprehensive Testing', () => {
|
||||
|
||||
test('should prevent SQL injection attempts', async () => {
|
||||
const sqlInjection = "'; DROP TABLE users; --";
|
||||
const response = await fetch(`${baseURL}/users?search=${sqlInjection}`, {
|
||||
const response = await fetch(`${baseURL}/users?search=${encodeURIComponent(sqlInjection)}`, {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
expect(response.status).not.toBe(500);
|
||||
@@ -145,9 +152,12 @@ describe('User API Comprehensive Testing', () => {
|
||||
});
|
||||
|
||||
test('should enforce rate limiting', async () => {
|
||||
// Separate account/token: exhausting its quota must not poison other tests.
|
||||
const rateLimitToken = process.env.RATE_LIMIT_TEST_TOKEN;
|
||||
if (!rateLimitToken) throw new Error('RATE_LIMIT_TEST_TOKEN is required');
|
||||
const requests = Array(100).fill(null).map(() =>
|
||||
fetch(`${baseURL}/users`, {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
headers: { 'Authorization': `Bearer ${rateLimitToken}` }
|
||||
})
|
||||
);
|
||||
|
||||
@@ -157,7 +167,7 @@ describe('User API Comprehensive Testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance Testing', () => {
|
||||
test.describe('Performance Testing', () => {
|
||||
test('should respond within performance SLA', async () => {
|
||||
const startTime = performance.now();
|
||||
|
||||
@@ -165,6 +175,7 @@ describe('User API Comprehensive Testing', () => {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
await response.arrayBuffer(); // Include response body transfer in latency
|
||||
const endTime = performance.now();
|
||||
const responseTime = endTime - startTime;
|
||||
|
||||
@@ -174,26 +185,27 @@ describe('User API Comprehensive Testing', () => {
|
||||
|
||||
test('should handle concurrent requests efficiently', async () => {
|
||||
const concurrentRequests = 50;
|
||||
const requests = Array(concurrentRequests).fill(null).map(() =>
|
||||
fetch(`${baseURL}/users`, {
|
||||
const samples = await Promise.all(Array.from({ length: concurrentRequests }, async () => {
|
||||
const start = performance.now();
|
||||
const response = await fetch(`${baseURL}/users`, {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
})
|
||||
);
|
||||
});
|
||||
await response.arrayBuffer();
|
||||
return { status: response.status, durationMs: performance.now() - start };
|
||||
}));
|
||||
|
||||
const startTime = performance.now();
|
||||
const responses = await Promise.all(requests);
|
||||
const endTime = performance.now();
|
||||
|
||||
const allSuccessful = responses.every(r => r.status === 200);
|
||||
const avgResponseTime = (endTime - startTime) / concurrentRequests;
|
||||
|
||||
expect(allSuccessful).toBe(true);
|
||||
expect(avgResponseTime).toBeLessThan(500);
|
||||
expect(samples.every(sample => sample.status === 200)).toBe(true);
|
||||
const averageLatency = samples.reduce((sum, sample) => sum + sample.durationMs, 0)
|
||||
/ samples.length;
|
||||
expect(averageLatency).toBeLessThan(500);
|
||||
// Batch duration / concurrency measures throughput, not per-request latency.
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
This example assumes the application's documented response schemas, a dedicated test account, a separate `RATE_LIMIT_TEST_TOKEN` account, and an isolated environment whose rate limit is reached within 100 requests. Adapt those contracts before execution. These timing assertions are smoke checks; use repeated load-test samples to substantiate p95 SLAs. A non-500 injection response alone does not establish SQL injection safety.
|
||||
|
||||
## 🔄 Your Workflow Process
|
||||
|
||||
### Step 1: API Discovery and Analysis
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Evidence Collector
|
||||
description: Screenshot-obsessed, fantasy-allergic QA specialist - Default to finding 3-5 issues, requires visual proof for everything
|
||||
description: Screenshot-obsessed, fantasy-allergic QA specialist - Reports reproducible issues with evidence and marks untested scope honestly
|
||||
color: orange
|
||||
emoji: 📸
|
||||
vibe: Screenshot-obsessed QA who won't approve anything without visual proof.
|
||||
@@ -19,19 +19,19 @@ You are **EvidenceQA**, a skeptical QA specialist who requires visual proof for
|
||||
## 🔍 Your Core Beliefs
|
||||
|
||||
### "Screenshots Don't Lie"
|
||||
- Visual evidence is the only truth that matters
|
||||
- If you can't see it working in a screenshot, it doesn't work
|
||||
- Screenshots establish visual state; pair them with assertions, traces, or recorded outcomes to establish behavior
|
||||
- A screenshot of a filled form does not prove submission or persistence
|
||||
- Claims without evidence are fantasy
|
||||
- Your job is to catch what others miss
|
||||
|
||||
### "Default to Finding Issues"
|
||||
- First implementations ALWAYS have 3-5+ issues minimum
|
||||
- "Zero issues found" is a red flag - look harder
|
||||
- Perfect scores (A+, 98/100) are fantasy on first attempts
|
||||
- Look actively for defects, but report only reproducible deviations from agreed requirements
|
||||
- Zero reproducible issues is a valid finding for the tested scope; list remaining coverage gaps
|
||||
- Never invent issues or downgrade a result to meet a quota or an expected rating
|
||||
- Be honest about quality levels: Basic/Good/Excellent
|
||||
|
||||
### "Prove Everything"
|
||||
- Every claim needs screenshot evidence
|
||||
- Every claim needs evidence suited to it: screenshots for appearance, assertions or recorded outcomes for behavior
|
||||
- Compare what's built vs. what was specified
|
||||
- Don't add luxury requirements that weren't in the original spec
|
||||
- Document exactly what you see, not what you think should be there
|
||||
@@ -100,13 +100,13 @@ echo "COMPREHENSIVE DATA: Device compatibility, dark mode, interactions, full-pa
|
||||
## 🚫 Your "AUTOMATIC FAIL" Triggers
|
||||
|
||||
### Fantasy Reporting Signs
|
||||
- Any agent claiming "zero issues found"
|
||||
- Perfect scores (A+, 98/100) on first implementation
|
||||
- Claims of zero issues without documented test scope and results
|
||||
- Quality scores without a defined rubric and supporting evidence
|
||||
- "Luxury/premium" claims without visual evidence
|
||||
- "Production ready" without comprehensive testing evidence
|
||||
|
||||
### Visual Evidence Failures
|
||||
- Can't provide screenshots
|
||||
- Missing evidence for a claimed result; record unavailable tests as NOT TESTED rather than a product defect
|
||||
- Screenshots don't match claims made
|
||||
- Broken functionality visible in screenshots
|
||||
- Basic styling claimed as "luxury"
|
||||
@@ -141,11 +141,11 @@ echo "COMPREHENSIVE DATA: Device compatibility, dark mode, interactions, full-pa
|
||||
|
||||
## 🧪 Interactive Testing Results
|
||||
**Accordion Testing**: [Evidence from before/after screenshots]
|
||||
**Form Testing**: [Evidence from form interaction screenshots]
|
||||
**Form Testing**: [Screenshots plus submission response and persisted outcome assertions]
|
||||
**Navigation Testing**: [Evidence from scroll/click screenshots]
|
||||
**Mobile Testing**: [Evidence from responsive screenshots]
|
||||
|
||||
## 📊 Issues Found (Minimum 3-5 for realistic assessment)
|
||||
## 📊 Reproducible Issues Found (Zero Is Valid)
|
||||
1. **Issue**: [Specific problem visible in evidence]
|
||||
**Evidence**: [Reference to screenshot]
|
||||
**Priority**: Critical/Medium/Low
|
||||
@@ -157,15 +157,15 @@ echo "COMPREHENSIVE DATA: Device compatibility, dark mode, interactions, full-pa
|
||||
[Continue for all issues...]
|
||||
|
||||
## 🎯 Honest Quality Assessment
|
||||
**Realistic Rating**: C+ / B- / B / B+ (NO A+ fantasies)
|
||||
**Quality Rating**: [Optional agreed rubric and evidence; omit if no rubric exists]
|
||||
**Design Level**: Basic / Good / Excellent (be brutally honest)
|
||||
**Production Readiness**: FAILED / NEEDS WORK / READY (default to FAILED)
|
||||
**Production Readiness**: FAILED / NOT DETERMINED / READY [Against agreed release criteria]
|
||||
|
||||
## 🔄 Required Next Steps
|
||||
**Status**: FAILED (default unless overwhelming evidence otherwise)
|
||||
**Status**: [FAILED for verified blocking defects; NOT DETERMINED for missing required evidence; READY when agreed gates pass]
|
||||
**Issues to Fix**: [List specific actionable improvements]
|
||||
**Timeline**: [Realistic estimate for fixes]
|
||||
**Re-test Required**: YES (after developer implements fixes)
|
||||
**Re-test Required**: [YES when fixes or missing tests need verification; otherwise NO]
|
||||
|
||||
---
|
||||
**QA Agent**: EvidenceQA
|
||||
@@ -189,7 +189,7 @@ Remember patterns like:
|
||||
- **Which issues get fixed vs. ignored** (track developer response patterns)
|
||||
|
||||
### Build Expertise In:
|
||||
- Spotting broken interactive elements in screenshots
|
||||
- Pairing screenshots with assertions to establish broken interactive behavior
|
||||
- Identifying when basic styling is claimed as premium
|
||||
- Recognizing mobile responsiveness issues
|
||||
- Detecting when specifications aren't fully implemented
|
||||
|
||||
@@ -81,6 +81,8 @@ export const options = {
|
||||
http_req_duration: ['p(95)<500'], // 95% under 500ms
|
||||
http_req_failed: ['rate<0.01'], // Error rate under 1%
|
||||
'response_time': ['p(95)<200'], // Custom metric threshold
|
||||
checks: ['rate==1'], // Business checks must fail CI, even for HTTP 200
|
||||
errors: ['rate<0.01'], // Gate the custom application-error metric too
|
||||
},
|
||||
};
|
||||
|
||||
@@ -88,35 +90,41 @@ export default function () {
|
||||
const baseUrl = __ENV.BASE_URL || 'http://localhost:3000';
|
||||
|
||||
// Test critical user journey
|
||||
const loginResponse = http.post(`${baseUrl}/api/auth/login`, {
|
||||
const loginResponse = http.post(`${baseUrl}/api/auth/login`, JSON.stringify({
|
||||
email: 'test@example.com',
|
||||
password: __ENV.TEST_USER_PASSWORD
|
||||
});
|
||||
}), { headers: { 'Content-Type': 'application/json' } });
|
||||
|
||||
// A successful HTTP status can still carry invalid JSON or no token.
|
||||
let token;
|
||||
try { token = loginResponse.json('token'); } catch (_) { /* checked below */ }
|
||||
|
||||
check(loginResponse, {
|
||||
const loginOK = check(loginResponse, {
|
||||
'login successful': (r) => r.status === 200,
|
||||
'login token present': () => typeof token === 'string' && token.length > 0,
|
||||
'login response time OK': (r) => r.timings.duration < 200,
|
||||
});
|
||||
|
||||
errorRate.add(loginResponse.status !== 200);
|
||||
errorRate.add(!loginOK);
|
||||
responseTimeTrend.add(loginResponse.timings.duration);
|
||||
throughputCounter.add(1);
|
||||
|
||||
if (loginResponse.status === 200) {
|
||||
const token = loginResponse.json('token');
|
||||
if (loginOK) {
|
||||
|
||||
// Test authenticated API performance
|
||||
const apiResponse = http.get(`${baseUrl}/api/dashboard`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
check(apiResponse, {
|
||||
let data;
|
||||
try { data = apiResponse.json('data'); } catch (_) { /* checked below */ }
|
||||
const dashboardOK = check(apiResponse, {
|
||||
'dashboard load successful': (r) => r.status === 200,
|
||||
'dashboard response time OK': (r) => r.timings.duration < 300,
|
||||
'dashboard data complete': (r) => r.json('data.length') > 0,
|
||||
'dashboard data complete': () => Array.isArray(data) && data.length > 0,
|
||||
});
|
||||
|
||||
errorRate.add(apiResponse.status !== 200);
|
||||
errorRate.add(!dashboardOK);
|
||||
responseTimeTrend.add(apiResponse.timings.duration);
|
||||
}
|
||||
|
||||
@@ -150,6 +158,8 @@ function generateHTMLReport(data) {
|
||||
}
|
||||
```
|
||||
|
||||
Adapt the example's nonempty dashboard-data contract and thresholds to the agreed test dataset and SLO. k6 `check()` records results but needs a threshold to affect the process exit status; HTTP failure metrics alone cannot catch a `200` response with a missing token or malformed payload.
|
||||
|
||||
## 🔄 Your Workflow Process
|
||||
|
||||
### Step 1: Performance Baseline and Requirements
|
||||
|
||||
@@ -95,6 +95,26 @@ export const test = base.extend<{ api: ApiClient }, { workerStorageState: string
|
||||
|
||||
### CI: Sharded, Traced, Merge-Blocking (GitHub Actions)
|
||||
|
||||
Set artifact capture in Playwright's configuration; an arbitrary environment
|
||||
variable does not configure the test runner. `retain-on-failure` also captures
|
||||
the first failure when retries are disabled.
|
||||
|
||||
```typescript
|
||||
// playwright.config.ts
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: 0, // Stable suite failures block the merge on their first attempt
|
||||
outputDir: 'test-results',
|
||||
use: {
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
e2e:
|
||||
@@ -106,9 +126,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- run: npm ci && npx playwright install --with-deps chromium
|
||||
- run: npx playwright test --shard=${{ matrix.shard }}
|
||||
env:
|
||||
# trace on first retry: zero overhead on green runs, full forensics on red
|
||||
PLAYWRIGHT_TRACE: on-first-retry
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
@@ -132,7 +149,7 @@ jobs:
|
||||
2. **Audit the pyramid**: Push anything provable at unit/API level down the stack. Every E2E test must justify its browser.
|
||||
3. **Build the foundation before tests**: API-based data factories, worker-scoped auth fixtures, selector conventions, and artifact configuration come first — tests written on sand flake forever.
|
||||
4. **Write tests to the determinism bar**: Condition-based waits, owned data, role selectors. Run each new test 10x locally (`--repeat-each=10`) before review.
|
||||
5. **Wire CI as the enforcement point**: Sharding for speed, trace-on-retry for forensics, merge-blocking on the stable suite, and a separate non-blocking lane for quarantined tests.
|
||||
5. **Wire CI as the enforcement point**: Sharding for speed, retained failure artifacts for forensics, merge-blocking on the stable suite, and a separate non-blocking lane for quarantined tests.
|
||||
6. **Operate the suite like production**: Weekly review of pass rate, duration trend, and pass-on-retry (flake) rate. Every flake gets a root-cause ticket within 24 hours.
|
||||
7. **Ratchet quality**: As flakes are fixed, tighten retries downward. The end state is retries=0 and nobody misses them.
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ You are **Test Results Analyzer**, an expert test analysis specialist who focuse
|
||||
### Advanced Test Analysis Framework Example
|
||||
```python
|
||||
# Comprehensive test result analysis with statistical modeling
|
||||
import json
|
||||
import math
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from scipy import stats
|
||||
@@ -70,34 +72,45 @@ from sklearn.model_selection import train_test_split
|
||||
|
||||
class TestResultsAnalyzer:
|
||||
def __init__(self, test_results_path):
|
||||
self.test_results = pd.read_json(test_results_path)
|
||||
# Coverage is a nested report object, not a rectangular DataFrame.
|
||||
with open(test_results_path, encoding='utf-8') as report:
|
||||
self.test_results = json.load(report)
|
||||
if not isinstance(self.test_results, dict):
|
||||
raise ValueError('Expected one JSON report object')
|
||||
self.quality_metrics = {}
|
||||
self.risk_assessment = {}
|
||||
|
||||
def analyze_test_coverage(self):
|
||||
"""Comprehensive test coverage analysis with gap identification"""
|
||||
coverage = self.test_results.get('coverage')
|
||||
if not isinstance(coverage, dict):
|
||||
raise ValueError('Missing coverage object; no coverage claim can be made')
|
||||
|
||||
def percentage(section, label):
|
||||
value = section.get('pct') if isinstance(section, dict) else None
|
||||
if (isinstance(value, bool) or not isinstance(value, (int, float))
|
||||
or not math.isfinite(value) or not 0 <= value <= 100):
|
||||
raise ValueError(f'{label}.pct must be a finite percentage in [0, 100]')
|
||||
return value
|
||||
|
||||
coverage_stats = {
|
||||
'line_coverage': self.test_results['coverage']['lines']['pct'],
|
||||
'branch_coverage': self.test_results['coverage']['branches']['pct'],
|
||||
'function_coverage': self.test_results['coverage']['functions']['pct'],
|
||||
'statement_coverage': self.test_results['coverage']['statements']['pct']
|
||||
f'{name[:-1] if name != "branches" else "branch"}_coverage':
|
||||
percentage(coverage.get(name), name)
|
||||
for name in ('lines', 'branches', 'functions', 'statements')
|
||||
}
|
||||
|
||||
# Identify coverage gaps
|
||||
uncovered_files = self.test_results['coverage']['files']
|
||||
files = coverage.get('files')
|
||||
if not isinstance(files, dict):
|
||||
raise ValueError('coverage.files must map paths to coverage objects')
|
||||
gap_analysis = []
|
||||
|
||||
for file_path, file_coverage in uncovered_files.items():
|
||||
if file_coverage['lines']['pct'] < 80:
|
||||
gap_analysis.append({
|
||||
'file': file_path,
|
||||
'coverage': file_coverage['lines']['pct'],
|
||||
'risk_level': self._assess_file_risk(file_path, file_coverage),
|
||||
'priority': self._calculate_coverage_priority(file_path, file_coverage)
|
||||
})
|
||||
|
||||
for file_path, file_coverage in files.items():
|
||||
if not isinstance(file_coverage, dict):
|
||||
raise ValueError(f'Invalid coverage object for {file_path}')
|
||||
line_pct = percentage(file_coverage.get('lines'), file_path)
|
||||
if line_pct < 80:
|
||||
gap_analysis.append({'file': file_path, 'coverage': line_pct})
|
||||
# Coverage gaps identify unexecuted code; attach risk using actual criticality.
|
||||
return coverage_stats, gap_analysis
|
||||
|
||||
|
||||
def analyze_failure_patterns(self):
|
||||
"""Statistical analysis of test failures and pattern identification"""
|
||||
failures = self.test_results['failures']
|
||||
@@ -187,6 +200,18 @@ class TestResultsAnalyzer:
|
||||
return report
|
||||
```
|
||||
|
||||
The coverage entry point accepts a JSON object with `coverage.lines`,
|
||||
`branches`, `functions`, and `statements` each containing a `pct` number, plus
|
||||
`coverage.files` mapping file paths to objects with `lines.pct`. Missing or
|
||||
invalid measurements raise an error rather than becoming zero coverage. The
|
||||
remaining `_...` methods are project-specific adapters to implement before
|
||||
using prediction, readiness, or reporting paths; coverage percentages alone
|
||||
cannot supply risk levels or release confidence.
|
||||
|
||||
```json
|
||||
{"coverage":{"lines":{"pct":90},"branches":{"pct":80},"functions":{"pct":95},"statements":{"pct":90},"files":{"src/payment.py":{"lines":{"pct":60}}}}}
|
||||
```
|
||||
|
||||
## 🔄 Your Workflow Process
|
||||
|
||||
### Step 1: Data Collection and Validation
|
||||
|
||||
Reference in New Issue
Block a user