create-agent-rig 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +466 -3
- package/README.md +267 -299
- package/package.json +8 -14
- package/packages/cli/dist/commands/create.js +56 -88
- package/packages/cli/dist/commands/doctor.js +213 -0
- package/packages/cli/dist/commands/init.js +151 -54
- package/packages/cli/dist/commands/integrations.js +468 -0
- package/packages/cli/dist/commands/memory.js +63 -4
- package/packages/cli/dist/commands/setup-wizard.js +61 -0
- package/packages/cli/dist/commands/uninstall.js +1223 -0
- package/packages/cli/dist/commands/upgrade.js +489 -89
- package/packages/cli/dist/index.js +657 -49
- package/packages/cli/dist/integrations/declaration.js +158 -0
- package/packages/cli/dist/integrations/doctor-guards.js +126 -0
- package/packages/cli/dist/integrations/doctor-workflow.js +25 -0
- package/packages/cli/dist/integrations/mcp-json.js +107 -0
- package/packages/cli/dist/integrations/memory-doctor.js +124 -0
- package/packages/cli/dist/integrations/registry.js +22 -0
- package/packages/cli/dist/integrations/spawn.js +228 -0
- package/packages/cli/dist/integrations/spec-kit.js +280 -0
- package/packages/cli/dist/integrations/verify.js +193 -0
- package/packages/cli/dist/integrations/windows-job.js +275 -0
- package/packages/cli/dist/lib/elevated-paths.js +74 -0
- package/packages/cli/dist/lib/install-set.js +1 -44
- package/packages/cli/dist/lib/manifest.js +86 -9
- package/packages/cli/dist/lib/prompts.js +0 -23
- package/packages/cli/dist/lib/safe-path.js +193 -2
- package/packages/cli/dist/lib/safe-text.js +78 -0
- package/packages/cli/dist/lib/substitute.js +1 -52
- package/packages/cli/dist/lib/summary.js +5 -5
- package/packages/cli/dist/templates.js +4 -14
- package/scripts/prepare.mjs +28 -12
- package/templates/agent-os/subagent-routing.json +4 -4
- package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +13 -9
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +31 -6
- package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +5 -5
- package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +3 -3
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -6
- package/templates/agent-os/universal/.claude/agents/implementation-agent.md +41 -0
- package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +8 -4
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +4 -2
- package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -2
- package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +83 -4
- package/templates/agent-os/universal/.claude/rules/autonomy.md +55 -27
- package/templates/agent-os/universal/.claude/rules/invariants.md +9 -8
- package/templates/agent-os/universal/.claude/rules/workflow.md +47 -22
- package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1 -1
- package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +12 -9
- package/templates/agent-os/universal/.claude/scripts/doctor.mjs +100 -2
- package/templates/agent-os/universal/.claude/scripts/lib/claim-records.mjs +236 -6
- package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +43 -0
- package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +18 -1
- package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +2 -2
- package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +273 -35
- package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +16 -0
- package/templates/agent-os/universal/.claude/settings.json +0 -8
- package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +13 -9
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +31 -6
- package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +5 -5
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +3 -3
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -2
- package/templates/agent-os/universal/.codex/agents/implementation-agent.toml +6 -0
- package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -2
- package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
- package/templates/agent-os/universal/.codex/agents/test-writer.toml +1 -1
- package/templates/agent-os/universal/.codex/hooks.json +0 -10
- package/templates/agent-os/universal/AGENTS.md +208 -107
- package/templates/agent-os/universal/CLAUDE.md +15 -162
- package/templates/agent-os/universal/PLAN.md +17 -10
- package/templates/agent-os/universal/docs/decisions/agents-md-canonical.md +313 -0
- package/templates/agent-os/universal/docs/decisions/codex-adapter.md +15 -10
- package/templates/agent-os/universal/docs/decisions/review-lanes.md +12 -8
- package/templates/agent-os/universal/docs/decisions/session-start-wire-format.md +206 -0
- package/templates/agent-os/universal/docs/decisions/subagent-routing.md +6 -3
- package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +235 -0
- package/templates/agent-os/universal/layers.json +25 -30
- package/templates/hash-history.json +73 -24
- package/templates/release-ledger.json +3 -1
- package/packages/cli/dist/lib/composition.js +0 -20
- package/packages/cli/dist/lib/targets.js +0 -28
- package/packages/cli/dist/policy/benchmark/corpus.js +0 -165
- package/packages/cli/dist/policy/core/adapter.js +0 -18
- package/packages/cli/dist/policy/core/coverage.js +0 -253
- package/packages/cli/dist/policy/core/decision-record.js +0 -287
- package/packages/cli/dist/policy/core/declaration.js +0 -127
- package/packages/cli/dist/policy/core/evidence-matrix.js +0 -94
- package/packages/cli/dist/policy/core/probe.js +0 -442
- package/packages/cli/dist/policy/core/registry.js +0 -115
- package/packages/cli/dist/policy/core/validation.js +0 -275
- package/packages/cli/dist/policy/core/vocabulary.js +0 -123
- package/packages/cli/dist/policy/harness/claude.js +0 -47
- package/packages/cli/dist/policy/harness/codex.js +0 -87
- package/packages/cli/dist/policy/harness/index.js +0 -15
- package/packages/cli/dist/policy/harness/shared-hooks.js +0 -28
- package/packages/cli/dist/policy/index.js +0 -17
- package/templates/agent-os/init/AGENTS.md +0 -201
- package/templates/agent-os/init/CLAUDE.md +0 -201
- package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +0 -117
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +0 -89
- package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +0 -117
- package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +0 -6
- package/templates/agent-os/stack/node-ts/.claude/hooks/dod-checks.json +0 -1
- package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +0 -85
- package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +0 -89
- package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +0 -68
- package/templates/agent-os/universal/.claude/rules/architecture.md +0 -81
- package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +0 -28
- package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +0 -90
- package/templates/skeleton/aws-serverless/README.md +0 -181
- package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +0 -17
- package/templates/skeleton/aws-serverless/apps/web/package.json +0 -19
- package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +0 -17
- package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +0 -96
- package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +0 -36
- package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +0 -23
- package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +0 -38
- package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +0 -14
- package/templates/skeleton/aws-serverless/eslint.config.mjs +0 -20
- package/templates/skeleton/aws-serverless/gitignore +0 -56
- package/templates/skeleton/aws-serverless/infra/bin/app.ts +0 -79
- package/templates/skeleton/aws-serverless/infra/cdk.json +0 -3
- package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +0 -248
- package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +0 -45
- package/templates/skeleton/aws-serverless/infra/package.json +0 -18
- package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +0 -301
- package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +0 -137
- package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +0 -120
- package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +0 -44
- package/templates/skeleton/aws-serverless/package.json +0 -30
- package/templates/skeleton/aws-serverless/packages/core/package.json +0 -11
- package/templates/skeleton/aws-serverless/packages/core/src/events.ts +0 -14
- package/templates/skeleton/aws-serverless/packages/core/src/index.ts +0 -15
- package/templates/skeleton/aws-serverless/packages/core/src/note.ts +0 -69
- package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +0 -23
- package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +0 -101
- package/templates/skeleton/aws-serverless/packages/db/package.json +0 -14
- package/templates/skeleton/aws-serverless/packages/db/src/client.ts +0 -17
- package/templates/skeleton/aws-serverless/packages/db/src/index.ts +0 -2
- package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +0 -52
- package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +0 -91
- package/templates/skeleton/aws-serverless/packages/shared/package.json +0 -11
- package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +0 -17
- package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +0 -33
- package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +0 -3
- package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +0 -20
- package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +0 -26
- package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +0 -28
- package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +0 -19
- package/templates/skeleton/aws-serverless/pnpm-lock.yaml +0 -2855
- package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +0 -14
- package/templates/skeleton/aws-serverless/services/api/package.json +0 -15
- package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +0 -26
- package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +0 -69
- package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +0 -37
- package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +0 -12
- package/templates/skeleton/aws-serverless/services/api/src/main.ts +0 -21
- package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +0 -41
- package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +0 -14
- package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +0 -211
- package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +0 -45
- package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +0 -122
- package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +0 -22
- package/templates/skeleton/aws-serverless/services/worker/package.json +0 -12
- package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +0 -15
- package/templates/skeleton/aws-serverless/services/worker/src/main.ts +0 -7
- package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +0 -37
- package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +0 -61
- package/templates/skeleton/aws-serverless/tsconfig.base.json +0 -15
- package/templates/skeleton/aws-serverless/tsconfig.json +0 -16
- package/templates/skeleton/aws-serverless/vitest.config.ts +0 -14
- package/templates/skeleton/node-service/.github/workflows/ci.yml +0 -27
- package/templates/skeleton/node-service/.github/workflows/deploy.yml +0 -29
- package/templates/skeleton/node-service/README.md +0 -104
- package/templates/skeleton/node-service/apps/web/next.config.mjs +0 -17
- package/templates/skeleton/node-service/apps/web/package.json +0 -19
- package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +0 -17
- package/templates/skeleton/node-service/apps/web/src/app/page.tsx +0 -96
- package/templates/skeleton/node-service/apps/web/src/lib/api.ts +0 -29
- package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +0 -23
- package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +0 -38
- package/templates/skeleton/node-service/apps/web/tsconfig.json +0 -14
- package/templates/skeleton/node-service/eslint.config.mjs +0 -20
- package/templates/skeleton/node-service/gitignore +0 -54
- package/templates/skeleton/node-service/package.json +0 -30
- package/templates/skeleton/node-service/packages/core/package.json +0 -11
- package/templates/skeleton/node-service/packages/core/src/events.ts +0 -14
- package/templates/skeleton/node-service/packages/core/src/index.ts +0 -15
- package/templates/skeleton/node-service/packages/core/src/note.ts +0 -69
- package/templates/skeleton/node-service/packages/core/test/events.test.ts +0 -23
- package/templates/skeleton/node-service/packages/core/test/note.test.ts +0 -101
- package/templates/skeleton/node-service/packages/db/package.json +0 -12
- package/templates/skeleton/node-service/packages/db/src/index.ts +0 -1
- package/templates/skeleton/node-service/packages/db/src/note-store.ts +0 -100
- package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +0 -100
- package/templates/skeleton/node-service/packages/shared/package.json +0 -11
- package/templates/skeleton/node-service/packages/shared/src/env.ts +0 -17
- package/templates/skeleton/node-service/packages/shared/src/errors.ts +0 -33
- package/templates/skeleton/node-service/packages/shared/src/index.ts +0 -3
- package/templates/skeleton/node-service/packages/shared/src/logger.ts +0 -20
- package/templates/skeleton/node-service/packages/shared/test/env.test.ts +0 -26
- package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +0 -28
- package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +0 -19
- package/templates/skeleton/node-service/pnpm-lock.yaml +0 -2402
- package/templates/skeleton/node-service/pnpm-workspace.yaml +0 -13
- package/templates/skeleton/node-service/scripts/build-artifact.mjs +0 -34
- package/templates/skeleton/node-service/services/api/package.json +0 -17
- package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +0 -23
- package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +0 -40
- package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +0 -23
- package/templates/skeleton/node-service/services/api/src/main.ts +0 -40
- package/templates/skeleton/node-service/services/api/src/server.ts +0 -172
- package/templates/skeleton/node-service/services/api/src/static-dir.ts +0 -20
- package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +0 -30
- package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +0 -14
- package/templates/skeleton/node-service/services/api/test/artifact.test.ts +0 -92
- package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +0 -64
- package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +0 -43
- package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +0 -48
- package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +0 -40
- package/templates/skeleton/node-service/services/api/test/package-manager.ts +0 -51
- package/templates/skeleton/node-service/services/api/test/server.test.ts +0 -208
- package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +0 -32
- package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +0 -29
- package/templates/skeleton/node-service/services/worker/package.json +0 -16
- package/templates/skeleton/node-service/services/worker/src/main.ts +0 -28
- package/templates/skeleton/node-service/services/worker/src/spool.ts +0 -60
- package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +0 -38
- package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +0 -34
- package/templates/skeleton/node-service/services/worker/test/spool.test.ts +0 -76
- package/templates/skeleton/node-service/tsconfig.base.json +0 -15
- package/templates/skeleton/node-service/tsconfig.json +0 -13
- package/templates/skeleton/node-service/vitest.config.ts +0 -12
|
@@ -680,6 +680,23 @@ export const escalate = async (ticket, diagnosis, { env = process.env } = {}) =>
|
|
|
680
680
|
return { ok: true };
|
|
681
681
|
};
|
|
682
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Jira refuses a summary longer than this, and creates nothing: `400
|
|
685
|
+
* {"errors":{"summary":"Summary can't exceed 255 characters."}}` (RP-121). The
|
|
686
|
+
* summary is therefore a prefix; the full `change` stays in the description,
|
|
687
|
+
* where the fingerprint lives too, so the dedupe does not depend on the cut.
|
|
688
|
+
* The cut never leaves half of a surrogate pair behind. Pinned in
|
|
689
|
+
* `test/template/queue-jira.test.ts` › "bounds the POSTed summary so a
|
|
690
|
+
* 2186-character change still files, keeping the full text in the body".
|
|
691
|
+
*/
|
|
692
|
+
const MAX_SUMMARY_LENGTH = 255;
|
|
693
|
+
|
|
694
|
+
const boundedSummary = (text) => {
|
|
695
|
+
if (text.length <= MAX_SUMMARY_LENGTH) return text;
|
|
696
|
+
const cut = text.slice(0, MAX_SUMMARY_LENGTH);
|
|
697
|
+
return /[\uD800-\uDBFF]$/.test(cut) ? cut.slice(0, -1) : cut;
|
|
698
|
+
};
|
|
699
|
+
|
|
683
700
|
/**
|
|
684
701
|
* 🔴 INVARIANT 2: the agent never creates its own work. A proposal is labelled
|
|
685
702
|
* `triage`, which `buildJql` excludes explicitly, and it never receives a ready
|
|
@@ -689,7 +706,7 @@ export const triageItemFor = (proposal) => {
|
|
|
689
706
|
validateProposal(proposal);
|
|
690
707
|
const fingerprint = fingerprintOf(proposal);
|
|
691
708
|
return {
|
|
692
|
-
title: `proposal: ${proposal.change}
|
|
709
|
+
title: boundedSummary(`proposal: ${proposal.change}`),
|
|
693
710
|
body: [
|
|
694
711
|
`- finding — ${proposal.finding}`,
|
|
695
712
|
`- part to change — ${proposal.part}`,
|
|
@@ -138,7 +138,7 @@ export const recordCompletedTier = ({ changedFiles, projectRoot, statePath, runD
|
|
|
138
138
|
if (!declared || declared.length === 0) {
|
|
139
139
|
throw new Error(
|
|
140
140
|
'nothing in this project declares an elevated path, so no tier can be ' +
|
|
141
|
-
'computed: add an `elevated-paths` block to
|
|
141
|
+
'computed: add an `elevated-paths` block to AGENTS.md or a rule file. ' +
|
|
142
142
|
'Treating the absence as `normal` would ration on a declaration that ' +
|
|
143
143
|
'does not exist.',
|
|
144
144
|
);
|
|
@@ -157,7 +157,7 @@ export const recordCompletedTier = ({ changedFiles, projectRoot, statePath, runD
|
|
|
157
157
|
// and silently relocating it would make it useless.
|
|
158
158
|
//
|
|
159
159
|
// Note the asymmetry, which is deliberate: the DECLARATION is read from the
|
|
160
|
-
// given `projectRoot` (the worktree's own `
|
|
160
|
+
// given `projectRoot` (the worktree's own `AGENTS.md` is the rulebook the
|
|
161
161
|
// change was written against), while the STATE goes to the checkout that
|
|
162
162
|
// outlives the task.
|
|
163
163
|
const file = statePath ?? join(mainCheckoutRoot(projectRoot), '.claude', 'queue.state.json');
|
|
@@ -22,10 +22,14 @@ import { dirname, join } from 'node:path';
|
|
|
22
22
|
// Imported, never copied: one home for the elevated declaration and one home for
|
|
23
23
|
// the lane rule. A second copy of either would drift, and each copy would keep
|
|
24
24
|
// passing on its own.
|
|
25
|
-
// `readDeclaredPaths` unions
|
|
26
|
-
//
|
|
27
|
-
//
|
|
25
|
+
// `readDeclaredPaths` unions AGENTS.md (the canonical rulebook since RP-186)
|
|
26
|
+
// and CLAUDE.md with every .claude/rules/*.md declaration, so this sweep sees
|
|
27
|
+
// exactly what the gate sweep sees — including the paths a stack layer
|
|
28
|
+
// contributes for its own shape.
|
|
28
29
|
import { elevatedPathsIn, laneOf, readDeclaredPaths } from './detect-missed-gate.mjs';
|
|
30
|
+
// The one credential vocabulary (`guard-secret-file`, the commit sweep and this
|
|
31
|
+
// diagnostic all read it): a second list of token shapes here would drift.
|
|
32
|
+
import { SECRET_VALUE_PATTERNS } from './lib/secrets.mjs';
|
|
29
33
|
|
|
30
34
|
/**
|
|
31
35
|
* The audit trail for work that already happened: a record born **closed**.
|
|
@@ -210,37 +214,261 @@ const parseArgs = (argv) => {
|
|
|
210
214
|
|
|
211
215
|
const daysAgo = (n) => new Date(Date.now() - n * 86_400_000).toISOString().slice(0, 10);
|
|
212
216
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
217
|
+
// --- Acquisition (RP-99) --------------------------------------------------------
|
|
218
|
+
//
|
|
219
|
+
// Two calls, not one. `gh pr list --json` does not know `authorAssociation` on
|
|
220
|
+
// every supported `gh` (2.71.2 answers `Unknown JSON field`), so the BASE
|
|
221
|
+
// projection asks only for fields every `gh` has, and the association — the
|
|
222
|
+
// trust signal — is fetched afterwards through one GraphQL query. When that
|
|
223
|
+
// second step fails for any reason the base list is returned WITHOUT the field
|
|
224
|
+
// and `reconcile()` treats those PRs as untrusted, and an answer is kept only
|
|
225
|
+
// when it names the same pull request URL the base list did — see the
|
|
226
|
+
// generator's test/template/reconcile-acquisition.test.ts (absent in a generated rig)
|
|
227
|
+
// › "reconcile() treats a PR that lost authorAssociation to a failed enrichment as untrusted when it crosses an elevated path"
|
|
228
|
+
// and › "drops an association whose answer names a different pull request URL — another host or repository never becomes trust".
|
|
229
|
+
|
|
230
|
+
/** The base projection: fields every supported `gh` serves. */
|
|
231
|
+
const BASE_FIELDS = ['number', 'title', 'body', 'headRefName', 'mergedAt', 'url', 'files', 'changedFiles'];
|
|
232
|
+
/** What GitHub returns for `authorAssociation`: an upper-case enum word. */
|
|
233
|
+
const ASSOCIATION_SHAPE = /^[A-Z_]{1,32}$/;
|
|
234
|
+
const MAX_PRS = 100;
|
|
235
|
+
|
|
236
|
+
/** The default runner: one `gh` child, stdout as text, a thrown error otherwise. */
|
|
237
|
+
const execGh = (file, args) =>
|
|
238
|
+
execFileSync(file, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
239
|
+
|
|
240
|
+
/** A base record the rest of this script can read: an object with a positive PR number. */
|
|
241
|
+
const isPrRecord = (row) =>
|
|
242
|
+
row !== null &&
|
|
243
|
+
typeof row === 'object' &&
|
|
244
|
+
!Array.isArray(row) &&
|
|
245
|
+
Number.isSafeInteger(row.number) &&
|
|
246
|
+
row.number > 0;
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The merged PRs since `since`, plus the warnings the acquisition produced.
|
|
250
|
+
*
|
|
251
|
+
* Throws only from the BASE list — the caller classifies that with
|
|
252
|
+
* `classifyAcquisitionFailure`. A parsed base list that is not an array is
|
|
253
|
+
* thrown with `notAnArray: true` (JSON.parse succeeded, so a SyntaxError would
|
|
254
|
+
* misdescribe it). Enrichment never throws: it degrades to a warning.
|
|
255
|
+
*/
|
|
256
|
+
export const fetchMergedPrs = (since, { exec = execGh } = {}) => {
|
|
257
|
+
const raw = exec('gh', [
|
|
258
|
+
'pr',
|
|
259
|
+
'list',
|
|
260
|
+
'--state',
|
|
261
|
+
'merged',
|
|
262
|
+
'--limit',
|
|
263
|
+
String(MAX_PRS),
|
|
264
|
+
'--search',
|
|
265
|
+
`merged:>=${since}`,
|
|
266
|
+
'--json',
|
|
267
|
+
BASE_FIELDS.join(','),
|
|
268
|
+
]);
|
|
269
|
+
const parsed = JSON.parse(raw);
|
|
270
|
+
if (!Array.isArray(parsed)) {
|
|
271
|
+
throw Object.assign(new Error('gh pr list answered with something other than a JSON array'), {
|
|
272
|
+
notAnArray: true,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const warnings = [];
|
|
277
|
+
const prs = [];
|
|
278
|
+
for (const row of parsed.slice(0, MAX_PRS)) {
|
|
279
|
+
if (!isPrRecord(row)) {
|
|
280
|
+
warnings.push('a merged-PR record without a positive numeric `number` was dropped from the sweep');
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
// Copy the base fields only; whatever else `gh` returned does not travel.
|
|
284
|
+
const record = {};
|
|
285
|
+
for (const field of BASE_FIELDS) if (row[field] !== undefined) record[field] = row[field];
|
|
286
|
+
prs.push(record);
|
|
287
|
+
}
|
|
288
|
+
if (prs.length === 0) return { prs, warnings };
|
|
289
|
+
|
|
290
|
+
const associations = fetchAuthorAssociations(prs, exec);
|
|
291
|
+
if (associations === null) {
|
|
292
|
+
warnings.push(
|
|
293
|
+
'author association unavailable for this window — every external merge below is treated as untrusted origin',
|
|
241
294
|
);
|
|
242
|
-
|
|
295
|
+
return { prs, warnings };
|
|
296
|
+
}
|
|
297
|
+
for (const pr of prs) {
|
|
298
|
+
const answer = associations.get(pr.number);
|
|
299
|
+
if (answer === undefined) continue;
|
|
300
|
+
// `gh api graphql` asks its own default host, not the remote's: an answer
|
|
301
|
+
// about another pull request must never lend its association to this one.
|
|
302
|
+
if (typeof pr.url !== 'string' || answer.url !== pr.url) {
|
|
303
|
+
warnings.push(`PR #${pr.number}: an author association whose pull request URL does not match the listed one was dropped`);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const value = answer.authorAssociation;
|
|
307
|
+
if (typeof value === 'string' && ASSOCIATION_SHAPE.test(value)) pr.authorAssociation = value;
|
|
308
|
+
else warnings.push(`PR #${pr.number}: an author association of an unexpected shape was dropped`);
|
|
243
309
|
}
|
|
310
|
+
return { prs, warnings };
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* `number → { url, authorAssociation }` for the listed PRs through one GraphQL
|
|
315
|
+
* query, or `null` when the answer cannot be trusted: a failed call, unusable
|
|
316
|
+
* JSON, a repository name of an unexpected shape. Never throws.
|
|
317
|
+
*/
|
|
318
|
+
const fetchAuthorAssociations = (prs, exec) => {
|
|
319
|
+
try {
|
|
320
|
+
const repo = JSON.parse(exec('gh', ['repo', 'view', '--json', 'nameWithOwner']));
|
|
321
|
+
const nameWithOwner = typeof repo?.nameWithOwner === 'string' ? repo.nameWithOwner : '';
|
|
322
|
+
const parts = nameWithOwner.split('/');
|
|
323
|
+
if (parts.length !== 2 || !parts.every((part) => /^[A-Za-z0-9_.-]{1,100}$/.test(part))) return null;
|
|
324
|
+
const [owner, name] = parts;
|
|
325
|
+
const fields = prs
|
|
326
|
+
.map((pr) => `pr${pr.number}: pullRequest(number: ${pr.number}) { url authorAssociation }`)
|
|
327
|
+
.join(' ');
|
|
328
|
+
const query = `query { repository(owner: "${owner}", name: "${name}") { ${fields} } }`;
|
|
329
|
+
const answer = JSON.parse(exec('gh', ['api', 'graphql', '-f', `query=${query}`]));
|
|
330
|
+
const repository = answer?.data?.repository;
|
|
331
|
+
if (repository === null || typeof repository !== 'object') return null;
|
|
332
|
+
const out = new Map();
|
|
333
|
+
for (const pr of prs) {
|
|
334
|
+
const node = repository[`pr${pr.number}`];
|
|
335
|
+
if (node === null || typeof node !== 'object' || node.authorAssociation === undefined) continue;
|
|
336
|
+
out.set(pr.number, { url: node.url, authorAssociation: node.authorAssociation });
|
|
337
|
+
}
|
|
338
|
+
return out;
|
|
339
|
+
} catch {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// --- Failure diagnostics (RP-99) -----------------------------------------------
|
|
345
|
+
|
|
346
|
+
const CAUSES = Object.freeze({
|
|
347
|
+
'gh-missing': 'the `gh` CLI could not be started (not installed, or not on PATH)',
|
|
348
|
+
'gh-unauthenticated': 'the `gh` CLI is not authenticated for this host — run `gh auth login`',
|
|
349
|
+
'api-unreachable': 'the GitHub API could not be reached',
|
|
350
|
+
'unsupported-projection': 'this `gh` version does not serve a field the sweep asked for',
|
|
351
|
+
'malformed-response': '`gh` answered, but not with the JSON array the sweep expects',
|
|
352
|
+
unknown: 'the cause is unknown',
|
|
353
|
+
});
|
|
354
|
+
const STDERR_UNAUTHENTICATED = /gh auth login|not logged in|HTTP 401|authentication required/i;
|
|
355
|
+
const STDERR_UNREACHABLE =
|
|
356
|
+
/dial tcp|ENOTFOUND|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|HTTP 5\d\d|could not connect|connection refused/i;
|
|
357
|
+
const STDERR_UNSUPPORTED = /Unknown JSON field/i;
|
|
358
|
+
// ESC-led sequences (CSI, OSC and the single-character escapes); the
|
|
359
|
+
// whitespace controls, which become a space where they sit next to whitespace
|
|
360
|
+
// and a mark where they sit inside a run (so they cannot split a token into
|
|
361
|
+
// pieces too short to match); and every other character a terminal would not
|
|
362
|
+
// print as itself — the remaining C0/C1 controls except the line feed the next
|
|
363
|
+
// step splits on, the invisible format characters (zero-width, bidi,
|
|
364
|
+
// byte-order mark), the line and paragraph separators, and U+E000, the mark
|
|
365
|
+
// they are all replaced with.
|
|
366
|
+
// eslint-disable-next-line no-control-regex -- the pattern exists to remove these very bytes
|
|
367
|
+
const TERMINAL_SEQUENCES = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-Z\\-_])/g;
|
|
368
|
+
const WHITESPACE_CONTROLS = /[\t\v\f\r]/g;
|
|
369
|
+
const WHITESPACE_CONTROLS_INSIDE_A_RUN = /(?<=\S)[\t\v\f\r]+(?=\S)/g;
|
|
370
|
+
// eslint-disable-next-line no-control-regex -- likewise
|
|
371
|
+
const HIDDEN_CHARACTERS = /[\x00-\x08\x0E-\x1F\x7F-\x9F\p{Cf}\p{Zl}\p{Zp}\uE000]/gu;
|
|
372
|
+
const MARK = '\uE000';
|
|
373
|
+
// No leading word boundary: a keyword glued to the word before must still be
|
|
374
|
+
// found, and over-redacting prose is the safe way to be wrong here.
|
|
375
|
+
const AUTHORIZATION_VALUE = /(?:Bearer|Basic|token)\s+[A-Za-z0-9._~+/=-]{8,}|Authorization:\s*[^\s]+(?:\s+[^\s]+)?/gi;
|
|
376
|
+
const DIAGNOSTIC_CAP = 200;
|
|
377
|
+
// Redaction runs on at most this much of the line, and the 200-character cut
|
|
378
|
+
// comes after it: cutting first can shorten a token below what its pattern
|
|
379
|
+
// needs and print the prefix. When the window itself cuts the line, the last
|
|
380
|
+
// whitespace-free run is dropped before redaction, so no token is ever
|
|
381
|
+
// shortened by a cut.
|
|
382
|
+
const REDACTION_WINDOW = 4096;
|
|
383
|
+
// A whitespace-free run that had a hidden character removed from it is masked
|
|
384
|
+
// whole once what is left is at least this long — the shortest value the
|
|
385
|
+
// vocabulary redacts, an authorization value. A removed character can glue a
|
|
386
|
+
// token to the word before it or split it into pieces too short to match, and
|
|
387
|
+
// neither can be told apart from honest text, so such a run is not trusted to
|
|
388
|
+
// the patterns at all.
|
|
389
|
+
const MARKED_RUN_MIN = 8;
|
|
390
|
+
// A masked run that ended in one of these keywords takes the next run with it:
|
|
391
|
+
// the value that followed the keyword can no longer be recognised on its own.
|
|
392
|
+
const AUTHORIZATION_KEYWORD_END = /(?:bearer|basic|token|authorization:)$/i;
|
|
393
|
+
|
|
394
|
+
/** `text` without its trailing whitespace-free run. One backward pass. */
|
|
395
|
+
const dropTrailingRun = (text) => {
|
|
396
|
+
let end = text.length;
|
|
397
|
+
while (end > 0 && !/\s/.test(text[end - 1])) end -= 1;
|
|
398
|
+
return text.slice(0, end);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const redactPatterns = (text) => {
|
|
402
|
+
let out = text.replace(AUTHORIZATION_VALUE, '[redacted]');
|
|
403
|
+
for (const { pattern } of SECRET_VALUE_PATTERNS) {
|
|
404
|
+
out = out.replace(new RegExp(pattern.source, `${pattern.flags.replace('g', '')}g`), '[redacted]');
|
|
405
|
+
}
|
|
406
|
+
return out;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* One printable line of a subprocess's stderr: no terminal sequences, control
|
|
411
|
+
* or invisible format characters, the first non-empty line only, every run a
|
|
412
|
+
* hidden character was removed from masked when it is long enough to be a
|
|
413
|
+
* credential, the credential shapes the shared vocabulary knows — plus HTTP
|
|
414
|
+
* authorization values — replaced by `[redacted]`, and then at most 200
|
|
415
|
+
* characters.
|
|
416
|
+
*/
|
|
417
|
+
export const sanitizeDiagnostic = (text) => {
|
|
418
|
+
const source = typeof text === 'string' ? text : String(text ?? '');
|
|
419
|
+
const marked = source
|
|
420
|
+
.replace(TERMINAL_SEQUENCES, MARK)
|
|
421
|
+
.replace(HIDDEN_CHARACTERS, MARK)
|
|
422
|
+
.replace(WHITESPACE_CONTROLS_INSIDE_A_RUN, MARK)
|
|
423
|
+
.replace(WHITESPACE_CONTROLS, ' ');
|
|
424
|
+
const line = marked.split('\n').find((candidate) => candidate.replaceAll(MARK, '').trim() !== '') ?? '';
|
|
425
|
+
const windowed = line.length > REDACTION_WINDOW ? dropTrailingRun(line.slice(0, REDACTION_WINDOW)) : line;
|
|
426
|
+
let maskNext = false;
|
|
427
|
+
const masked = windowed
|
|
428
|
+
.split(/(\s+)/)
|
|
429
|
+
.map((run) => {
|
|
430
|
+
if (run.trim() === '') return run;
|
|
431
|
+
const visible = run.replaceAll(MARK, '');
|
|
432
|
+
const hidden = run.includes(MARK) && visible.length >= MARKED_RUN_MIN;
|
|
433
|
+
const mask = hidden || maskNext;
|
|
434
|
+
maskNext = mask && AUTHORIZATION_KEYWORD_END.test(visible);
|
|
435
|
+
return mask ? '[redacted]' : visible;
|
|
436
|
+
})
|
|
437
|
+
.join('');
|
|
438
|
+
return redactPatterns(masked).trim().slice(0, DIAGNOSTIC_CAP);
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The cause the evidence supports, and nothing more. `detail` is the sanitized
|
|
443
|
+
* first non-empty stderr line, whatever the cause, or `null` when there was none. An
|
|
444
|
+
* `unknown` failure with no stderr falls back to the error's own message; the
|
|
445
|
+
* other causes never do, because a parse error's message quotes `gh` output.
|
|
446
|
+
*/
|
|
447
|
+
export const classifyAcquisitionFailure = (error) => {
|
|
448
|
+
const stderr = typeof error?.stderr === 'string' ? error.stderr : '';
|
|
449
|
+
const detail = stderr.trim() === '' ? null : sanitizeDiagnostic(stderr);
|
|
450
|
+
if (error?.code === 'ENOENT') return { cause: 'gh-missing', detail };
|
|
451
|
+
if (error instanceof SyntaxError || error?.notAnArray === true) {
|
|
452
|
+
return { cause: 'malformed-response', detail };
|
|
453
|
+
}
|
|
454
|
+
if (STDERR_UNSUPPORTED.test(stderr)) return { cause: 'unsupported-projection', detail };
|
|
455
|
+
if (STDERR_UNAUTHENTICATED.test(stderr)) return { cause: 'gh-unauthenticated', detail };
|
|
456
|
+
if (STDERR_UNREACHABLE.test(stderr)) return { cause: 'api-unreachable', detail };
|
|
457
|
+
if (detail === null && typeof error?.message === 'string' && error.message.trim() !== '') {
|
|
458
|
+
return { cause: 'unknown', detail: sanitizeDiagnostic(error.message) };
|
|
459
|
+
}
|
|
460
|
+
return { cause: 'unknown', detail };
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
/** The message the CLI prints for a failed acquisition: one cause, named. */
|
|
464
|
+
export const acquisitionFailureMessage = ({ cause, detail }) => {
|
|
465
|
+
const named = CAUSES[cause] ?? CAUSES.unknown;
|
|
466
|
+
return (
|
|
467
|
+
`lane reconciliation: could not list merged PRs — ${named}. ` +
|
|
468
|
+
'No lane was reconciled; do not record an empty `external lane` block from this run. ' +
|
|
469
|
+
'Use --input <file> to work offline.' +
|
|
470
|
+
(detail ? `\n ${detail}` : '')
|
|
471
|
+
);
|
|
244
472
|
};
|
|
245
473
|
|
|
246
474
|
/**
|
|
@@ -267,9 +495,19 @@ const invokedDirectly = () => {
|
|
|
267
495
|
if (invokedDirectly()) {
|
|
268
496
|
const args = parseArgs(process.argv.slice(2));
|
|
269
497
|
const projectRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
498
|
+
let prs;
|
|
499
|
+
let warnings = [];
|
|
500
|
+
if (args.input) {
|
|
501
|
+
prs = readInput(args.input, 'lane reconciliation');
|
|
502
|
+
} else {
|
|
503
|
+
try {
|
|
504
|
+
({ prs, warnings } = fetchMergedPrs(args.since ?? daysAgo(7)));
|
|
505
|
+
} catch (error) {
|
|
506
|
+
process.stderr.write(`${acquisitionFailureMessage(classifyAcquisitionFailure(error))}\n`);
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
for (const warning of warnings) process.stderr.write(`lane reconciliation: ${warning}\n`);
|
|
273
511
|
// Lane sorting still works without a declaration; only the elevated marks go
|
|
274
512
|
// missing, and they render as "no elevated path crossed" rather than lying.
|
|
275
513
|
const elevatedPaths = readDeclaredPaths(projectRoot) ?? [];
|
|
@@ -179,6 +179,7 @@ const parseArgs = (argv) => {
|
|
|
179
179
|
json: false,
|
|
180
180
|
actionChanged: null,
|
|
181
181
|
note: null,
|
|
182
|
+
mergeCommit: null,
|
|
182
183
|
bad: null,
|
|
183
184
|
};
|
|
184
185
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -201,6 +202,15 @@ const parseArgs = (argv) => {
|
|
|
201
202
|
const value = revisionOrNull(argv[++i]);
|
|
202
203
|
if (value === null) args.bad = arg;
|
|
203
204
|
else args.base = value;
|
|
205
|
+
}
|
|
206
|
+
else if (arg === '--merge-commit') {
|
|
207
|
+
// Read only at BEFORE_CLOSE (below); ignored elsewhere, the same way
|
|
208
|
+
// BEFORE_CLOSE ignores --base. A value starting with '-' would be read
|
|
209
|
+
// as another option, so it is rejected the same defensive way --ticket
|
|
210
|
+
// and --base already are.
|
|
211
|
+
const value = revisionOrNull(argv[++i]);
|
|
212
|
+
if (value === null) args.bad = arg;
|
|
213
|
+
else args.mergeCommit = value;
|
|
204
214
|
} else if (args.bad === null) args.bad = arg;
|
|
205
215
|
}
|
|
206
216
|
return args;
|
|
@@ -806,6 +816,12 @@ if (invokedDirectly()) {
|
|
|
806
816
|
// target SELECT pinned, so a missing `origin/master` cannot turn an
|
|
807
817
|
// otherwise current local rig into claim:scope drift.
|
|
808
818
|
targetSha: targetShaOf(claimRoot),
|
|
819
|
+
// Optional (RP-175): the SHA the caller resolved, from the tracker's own
|
|
820
|
+
// PR metadata, as THIS item's own merge commit. Omit it and a target
|
|
821
|
+
// that moved for any reason still holds, exactly as before this
|
|
822
|
+
// existed — see claim-records.mjs's isOwnMergeAdvance for what this
|
|
823
|
+
// does and does not prove.
|
|
824
|
+
mergeCommit: args.mergeCommit,
|
|
809
825
|
});
|
|
810
826
|
const result = {
|
|
811
827
|
...claim,
|
|
@@ -7,14 +7,6 @@
|
|
|
7
7
|
{
|
|
8
8
|
"matcher": "Write|Edit|MultiEdit|NotebookEdit|apply_patch",
|
|
9
9
|
"hooks": [
|
|
10
|
-
{
|
|
11
|
-
"type": "command",
|
|
12
|
-
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-core-purity.mjs\""
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
"type": "command",
|
|
16
|
-
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-web-boundary.mjs\""
|
|
17
|
-
},
|
|
18
10
|
{
|
|
19
11
|
"type": "command",
|
|
20
12
|
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-secret-file.mjs\""
|
|
@@ -28,7 +28,7 @@ same four steps with the diff as the code, and §4 carries what is different.
|
|
|
28
28
|
|
|
29
29
|
**What "rulebook prose" means here is not a new list** — it is the set
|
|
30
30
|
`.claude/rules/workflow.md` already uses for the `prose-reviewer` trigger: a rule
|
|
31
|
-
file, a skill, an agent spec, a decision record, `CLAUDE.md`, the README. Where a
|
|
31
|
+
file, a skill, an agent spec, a decision record, `CLAUDE.md`, `AGENTS.md`, the README. Where a
|
|
32
32
|
rulebook file exists twice (a template source and a generated copy), check the
|
|
33
33
|
**source**; the copy is composed from it. A comment in a test or a hook is in scope
|
|
34
34
|
too when it asserts behaviour — the file it lives in does not change what a claim is.
|
|
@@ -36,8 +36,8 @@ too when it asserts behaviour — the file it lives in does not change what a cl
|
|
|
36
36
|
🔴 **`PREMISE FALSE` belongs to the first entry point only.** At the second one the
|
|
37
37
|
claims are your own and the remedy is an edit, so a false one is not an escalation:
|
|
38
38
|
it is `UNMEASURED`'s neighbour — delete or correct the sentence and carry on. Reading
|
|
39
|
-
it as the escalation `loop` §6 defines would send a
|
|
40
|
-
over one sentence.
|
|
39
|
+
it as the escalation `loop` §6 (opt-in workflow layer) defines would send a
|
|
40
|
+
finished branch back to the queue over one sentence.
|
|
41
41
|
|
|
42
42
|
## Why it sits here and not in review
|
|
43
43
|
|
|
@@ -185,9 +185,13 @@ inside the block; both forms are the contract.
|
|
|
185
185
|
actually says, and `file`/`line` is the citation §3 requires.
|
|
186
186
|
- `PREMISES_HOLD` carries an empty `blockers` list; the other three name at
|
|
187
187
|
least one. A stop verdict without one is not an answer the caller can act on,
|
|
188
|
-
and the caller is what checks:
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
and the caller is what checks: `node .claude/scripts/verdict.mjs check
|
|
189
|
+
<report> check-premises` on what you return — `verdict.mjs` is Core and runs
|
|
190
|
+
standalone, so the caller is whichever session invoked this skill (this is a
|
|
191
|
+
Core skill, meant to run with no queue and no loop); the `loop` skill (opt-in
|
|
192
|
+
workflow layer) runs the same command the same way when it is the one that
|
|
193
|
+
invoked you. Nothing in this skill runs it — this skill writes nothing at
|
|
194
|
+
all.
|
|
191
195
|
- **`headSha` is the commit you read** — `git rev-parse HEAD` in the checkout
|
|
192
196
|
you examined. A premise check answers about a tree, and the tree moves.
|
|
193
197
|
|
|
@@ -231,6 +235,6 @@ is invisible to every gate downstream.
|
|
|
231
235
|
when a task starts building on an unchecked claim, and no artifact outlives the
|
|
232
236
|
step — so a run that skipped it and a run that passed it look identical
|
|
233
237
|
afterwards. That is the honest description of every rule of this shape here
|
|
234
|
-
(the `loop` skill says the same about its own
|
|
235
|
-
why the citation matters: a `file:line` in
|
|
236
|
-
later reader can re-check.
|
|
238
|
+
(the `loop` skill — opt-in workflow layer — says the same about its own
|
|
239
|
+
no-hand-feeding rule), and it is why the citation matters: a `file:line` in
|
|
240
|
+
the report is the one part of this a later reader can re-check.
|
|
@@ -12,7 +12,7 @@ decisions. This skill is the driver in between: what gets picked,
|
|
|
12
12
|
what keeps the loop going, what stops it, and where the report goes.
|
|
13
13
|
|
|
14
14
|
Per-task procedure: (worktree if another session may run) → `check-premises` on the
|
|
15
|
-
item → failing test first → implement → **`check-premises` again, on your own prose**
|
|
15
|
+
item → failing test first (`test-writer`) → implement (`implementation-agent`) → **`check-premises` again, on your own prose**
|
|
16
16
|
→ `pr-ship` → merge on the named criterion → verify the deployed surface if one
|
|
17
17
|
changed.
|
|
18
18
|
|
|
@@ -344,7 +344,7 @@ it only here. A tier outside that vocabulary **holds**, never releases
|
|
|
344
344
|
(`docs/decisions/spacing-rations-mechanisms.md`).
|
|
345
345
|
|
|
346
346
|
**The tier marker is a pre-filter, not the authority.** If an item passed as normal
|
|
347
|
-
and the work turns out to touch an elevated path (`
|
|
347
|
+
and the work turns out to touch an elevated path (`AGENTS.md` →
|
|
348
348
|
`elevated-paths`), run the gate anyway, record the verdict on the PR, and treat it
|
|
349
349
|
as this run's elevated item for spacing.
|
|
350
350
|
|
|
@@ -842,8 +842,8 @@ unbounded improvement list is another diary, and three forces a choice. Each nam
|
|
|
842
842
|
four things, and a proposal missing any of them is not ready to file:
|
|
843
843
|
|
|
844
844
|
1. the finding it came from, cited as the journal line it appears on;
|
|
845
|
-
2. the part to change — a skill, an agent spec, a hook, a rule file, `
|
|
846
|
-
the CI workflow;
|
|
845
|
+
2. the part to change — a skill, an agent spec, a hook, a rule file, `AGENTS.md`,
|
|
846
|
+
`CLAUDE.md`, the CI workflow;
|
|
847
847
|
3. the change, concretely enough to diff;
|
|
848
848
|
4. how the next run would prove it worked — the observation that would differ.
|
|
849
849
|
|
|
@@ -858,7 +858,7 @@ node --input-type=module -e '
|
|
|
858
858
|
const a = await import("./.claude/scripts/queue/plan-md.mjs"); // or github-issues / jira
|
|
859
859
|
console.log(await a.proposeTriage({
|
|
860
860
|
finding: "<the journal line it came from>",
|
|
861
|
-
part: "<skill | agent | hook | rule | CLAUDE.md | workflow>",
|
|
861
|
+
part: "<skill | agent | hook | rule | AGENTS.md | CLAUDE.md | workflow>",
|
|
862
862
|
change: "<concretely enough to diff>",
|
|
863
863
|
proof: "<the observation that would differ next run>",
|
|
864
864
|
// a pair: what the probe touched, and what is concluded from it. The
|
|
@@ -961,9 +961,34 @@ three poisons the only channel by which this project learns.
|
|
|
961
961
|
underneath it:
|
|
962
962
|
|
|
963
963
|
```bash
|
|
964
|
-
node .claude/scripts/revalidate.mjs --point BEFORE_CLOSE --ticket <item-id>
|
|
964
|
+
node .claude/scripts/revalidate.mjs --point BEFORE_CLOSE --ticket <item-id> \
|
|
965
|
+
--merge-commit "$(gh pr view <pr> --json mergeCommit -q .mergeCommit.oid)"
|
|
965
966
|
```
|
|
966
967
|
|
|
968
|
+
`--merge-commit` names the exact SHA the tracker's own PR metadata records as
|
|
969
|
+
THIS item's merge commit — one piece of evidence among several that let a
|
|
970
|
+
`targetSha` move which is nothing but that merge read as `CURRENT` instead
|
|
971
|
+
of `claim:scope` drift. Resolve it from the PR just merged, by name, never by
|
|
972
|
+
reading `git log` text, and only after the merge has landed and been
|
|
973
|
+
fetched. Omitting it (the item has not merged yet, or an older loop that
|
|
974
|
+
predates this flag) leaves any target movement holding exactly as it did
|
|
975
|
+
before this flag existed — it is purely additive to that default. Naming the
|
|
976
|
+
SHA is necessary but never sufficient: the exemption also requires the
|
|
977
|
+
target to have advanced by EXACTLY ONE commit — a genuine squash merge; a
|
|
978
|
+
merge commit or any multi-commit range still holds
|
|
979
|
+
(`test/template/revalidate.test.ts` (absent in a generated rig) › "a foreign
|
|
980
|
+
commit plus the real merge in the same range, --merge-commit correctly
|
|
981
|
+
naming the real one") — and it binds to THIS checkout: the named commit's
|
|
982
|
+
tree must equal `HEAD`'s tree here, and `HEAD` must still be the pre-merge
|
|
983
|
+
checkout, not one already fast-forwarded onto the merge
|
|
984
|
+
(`test/template/revalidate.test.ts` (absent in a generated rig) › "a single
|
|
985
|
+
FOREIGN commit named as --merge-commit does not exempt it — reproduces the
|
|
986
|
+
gate-hold attack and proves it now holds" and › "HEAD already at the merge
|
|
987
|
+
commit is vacuous, and still holds (a fast-forwarded checkout must not
|
|
988
|
+
self-satisfy the exemption)"). That binding stops a lazy or mistaken flag,
|
|
989
|
+
not an adversary who controls this run's own checkout — see
|
|
990
|
+
`isOwnMergeAdvance` in `claim-records.mjs` for the exact limit.
|
|
991
|
+
|
|
967
992
|
It compares the tracked claim's `scope` and `commentary` fingerprint sets;
|
|
968
993
|
commentary becomes hold-authoritative only here. Marker/take-up movement is
|
|
969
994
|
retained in evidence but cannot decide drift. A missing claim is
|
|
@@ -94,9 +94,9 @@ is a booby trap: someone will hit it, not understand it, and route around it.
|
|
|
94
94
|
- [ ] The rule is written down, with the hook named next to it
|
|
95
95
|
- [ ] The hook guards **one** invariant, and its scope is as narrow as the rule
|
|
96
96
|
|
|
97
|
-
##
|
|
97
|
+
## Choosing a candidate
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
Start from a rule that already matters in this project and has been violated or
|
|
100
|
+
missed in review. Do not invent a sample architecture rule just to exercise this
|
|
101
|
+
skill: the hook must protect a real local invariant, and its test must demonstrate
|
|
102
|
+
the exact violation it blocks.
|
|
@@ -257,7 +257,7 @@ blockers.
|
|
|
257
257
|
- `security-scanner` when the diff touches auth, secrets or configuration,
|
|
258
258
|
input parsing, file handling, new outbound calls, dependency changes;
|
|
259
259
|
- `prose-reviewer` when the diff touches a rule file, a skill, an agent spec,
|
|
260
|
-
a decision record under `docs/decisions/`, `CLAUDE.md` or the README — a
|
|
260
|
+
a decision record under `docs/decisions/`, `CLAUDE.md`, `AGENTS.md` or the README — a
|
|
261
261
|
rulebook that overstates its own enforcement fails silently and in the
|
|
262
262
|
direction of false confidence;
|
|
263
263
|
- an infrastructure review when it touches infrastructure (the stack layer
|
|
@@ -12,9 +12,9 @@ directories, with one `.git`. Without it, an unattended run and a hand-driven
|
|
|
12
12
|
session share a working tree and overwrite each other's edits.
|
|
13
13
|
|
|
14
14
|
**Use one when** anything else may touch this repo while you work — an
|
|
15
|
-
unattended `loop` run, a colleague, a second Claude
|
|
16
|
-
session on a quiet repo can just use a branch; the
|
|
17
|
-
optional is the branch, not the worktree.
|
|
15
|
+
unattended `loop` run (opt-in workflow layer), a colleague, a second Claude
|
|
16
|
+
session. A single attended session on a quiet repo can just use a branch; the
|
|
17
|
+
discipline that is never optional is the branch, not the worktree.
|
|
18
18
|
|
|
19
19
|
## Start
|
|
20
20
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
name = "code-reviewer"
|
|
2
|
-
description = "Reviews a completed change against the checklist before a PR is opened or merged. Use after any non-trivial implementation work, and always before opening a PR
|
|
2
|
+
description = "Reviews a completed change against the checklist before a PR is opened or merged. Use after any non-trivial implementation work, and always before opening a PR that reaches the `model` lane — everything the two cheap lanes do not claim (code, a rulebook document, an unclassifiable path, a derived artifact git does not report as drift, or anything a risk flag escalated), decided by `decision-router.mjs` where the opt-in workflow layer is installed, or by the same rule applied by hand otherwise. Blocking findings must be resolved, not argued with."
|
|
3
3
|
model = "gpt-5.6-sol"
|
|
4
4
|
model_reasoning_effort = "high"
|
|
5
5
|
sandbox_mode = "read-only"
|
|
6
|
-
developer_instructions = "You review changes. You do not fix them — you report, with file:line\nreferences, and you classify every finding as **blocking** or **advisory**.\n\n## Checklist (blocking findings)\n\n1. **Boundary violations** —
|
|
6
|
+
developer_instructions = "You review changes. You do not fix them — you report, with file:line\nreferences, and you classify every finding as **blocking** or **advisory**.\n\n## Checklist (blocking findings)\n\n1. **Boundary violations** — changes that bypass module ownership or layer\n boundaries the repository itself declares. See any applicable architecture\n rules in `.claude/rules/`.\n2. **Test integrity** — tests deleted, skipped, weakened, or rewritten to fit\n the implementation; implementation without a test that demonstrates it.\n3. **Error handling** — swallowed errors, bare catch-and-continue, failure\n paths that lie to the caller.\n4. **Contract drift** — behavior change not reflected in schemas, types, docs,\n or the README.\n5. **Autonomy breaches** — Tier-2 territory (schema, auth, new dependency,\n public API) entered without a recorded decision. See\n `.claude/rules/autonomy.md`.\n6. **Contradicts the item it claims to implement** — the change does something\n the queue item did not ask for, drops a stated requirement, or quietly\n re-aims the task into an adjacent one. Read the item first, then the diff.\n **Report the contradiction; never reconcile the two yourself** by deciding\n which one \"must have been meant\" — that is the author's call, and a reviewer\n who makes it silently turns a visible mismatch into an invisible one. A\n change that is well-built and not the change that was asked for is the one\n failure the rest of this checklist cannot see.\n\n **If the item was not handed to you, say so and stop there.** Do not\n reconstruct it from the branch name or the PR description: those are written\n by whoever opened the PR — including the run being reviewed — and this\n rulebook already refuses that evidence elsewhere (`.claude/rules/autonomy.md`).\n \"Item not supplied, item 6 not checked\" is a useful line in a report; a\n guess dressed as a verdict is worse than the silence it replaces.\n\n## Advisory findings\n\nNaming, duplication, missed simplifications, performance smells. Report them;\ndo not block on them.\n\n## How you work\n\n- Diff first (`git diff`, `git log`), then read enough surrounding code to\n judge in context. Review what changed, not the whole repo.\n- Quote the checklist item a blocking finding violates. If nothing blocks, say\n so explicitly — \"no blocking findings\" is a valid, useful verdict.\n- Do not request rewrites of working, tested code for style alone.\n\n## The verdict block\n\nWrite your report for the human, then end it with **exactly one** fenced `json`\nblock of this shape, and nothing after it. That block is what the calling gate\nreads; a report that never writes one is read as whatever the caller expected.\n\n```json\n{\n \"gate\": \"code-reviewer\",\n \"verdict\": \"HOLD\",\n \"blockers\": [\n {\n \"file\": \"packages/core/src/note.ts\",\n \"line\": 42,\n \"rule\": \"checklist item 2 — test integrity\",\n \"note\": \"the failing case was deleted rather than fixed\"\n }\n ],\n \"advisories\": [],\n \"evidence\": [\"diffed against origin/master\", \"queue item supplied\"],\n \"headSha\": \"9c1f0a7d4b3e2c5a8f6d0b9e7c4a1f2d3e5b6c70\"\n}\n```\n\n- `verdict` is `SHIP`, `HOLD` or `NOT_APPLICABLE` — no other word.\n- Every blocker names the `rule` it violates. `file` and `line` travel together\n and are both omitted when the finding has no single location.\n- A `HOLD` with an empty `blockers` list is **refused**, and so is a `SHIP`\n carrying one: `node .claude/scripts/verdict.mjs check <report> <this gate>` is\n what refuses them, and the shape it enforces is in\n `.claude/scripts/lib/verdict.mjs`. The gate name is what stops your answer\n being read as somebody else's.\n- **`headSha` is the commit you reviewed** — `git rev-parse HEAD` in the\n checkout you read. It is what lets `node .claude/scripts/verdict.mjs coverage\n <commit>` tell \"this gate answered for the commit being merged\" from \"it\n answered two pushes ago\". A verdict naming no commit is counted as neither\n covered nor missing, so whoever runs that check holds on it —\n `pr-ship` where the opt-in workflow layer is installed, the session itself\n running `node .claude/scripts/verdict.mjs coverage` by hand otherwise: no\n hook runs it either way, so skipping the gate skips this with it."
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
name = "implementation-agent"
|
|
2
|
+
description = "Writes the minimum production code that makes an existing failing test pass — the Green step of TDD, and the Refactor that follows it. Use for ordinary implementation work once test-writer has produced the failing test."
|
|
3
|
+
model = "gpt-5.6-terra"
|
|
4
|
+
model_reasoning_effort = "high"
|
|
5
|
+
sandbox_mode = "workspace-write"
|
|
6
|
+
developer_instructions = "You make a failing test pass. You are the Green step of TDD, and the Refactor\nthat keeps it green — never the Red step, and never the reviewer of your own\nwork.\n\n## Scope — hard boundaries\n\n- You start from a failing test that already exists. If there is none, stop\n and say so: the test comes first, from `test-writer`.\n- You never delete, skip, weaken or rewrite a test to make it pass. If a test\n looks wrong, stop and surface the conflict instead of editing it.\n- You change only what the task needs. Unrelated files, formatting sweeps and\n speculative abstractions are out of scope.\n- You do not commit, push, merge or open pull requests; the session that\n dispatched you owns the branch and the gates.\n\n## How you work\n\n1. Read the failing test and the code around it; follow the repository's\n rules in `AGENTS.md` and `.claude/rules/`.\n2. Write the smallest change that makes the test pass, in the style of the\n surrounding code.\n3. Run the new test, then the suite, lint and typecheck the rules name.\n Report a red check as information; never retry it until it passes.\n4. Refactor only with the tests green, then run them again.\n5. Report back: what you changed and why, which checks you ran with their\n results, and anything you found but did not change.\n\n## Judgment lines\n\n- When the change reaches an elevated area the task was not already tiered\n for, stop and say so before changing it.\n- When two rules in the repository conflict, stop and name both.\n- Prefer deleting code to adding it when both satisfy the test."
|