create-agent-rig 0.9.1 → 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 +419 -3
- package/README.md +267 -305
- 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 +111 -50
- package/packages/cli/dist/commands/integrations.js +468 -0
- 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 +472 -90
- package/packages/cli/dist/index.js +651 -46
- 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 +64 -5
- 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/state.mjs +2 -2
- package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +4 -3
- 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 +8 -4
- package/templates/release-ledger.json +2 -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
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { isSafeSubstitutionValue } from './safe-path.js';
|
|
4
|
+
import { isSafeSegment, isSafeSubstitutionValue, resolveWritableInside } from './safe-path.js';
|
|
5
|
+
import { hasControlCharacter } from './safe-text.js';
|
|
5
6
|
/**
|
|
6
7
|
* The install manifest: what this rig installed, at which version, and the
|
|
7
8
|
* hash each file had when it was written.
|
|
@@ -15,6 +16,28 @@ import { isSafeSubstitutionValue } from './safe-path.js';
|
|
|
15
16
|
* CI or on a colleague's machine is blind and falls back to the hash history.
|
|
16
17
|
*/
|
|
17
18
|
export const MANIFEST_REL = '.claude/.rig-manifest.json';
|
|
19
|
+
/** Every layer this release ships. */
|
|
20
|
+
export const ALL_LAYERS = ['process', 'workflow'];
|
|
21
|
+
/** What a fresh `init`/`create` installs when nothing opts into more. */
|
|
22
|
+
export const DEFAULT_LAYERS = ['process'];
|
|
23
|
+
/**
|
|
24
|
+
* What an OLD manifest — written before this field existed — is read as
|
|
25
|
+
* having installed.
|
|
26
|
+
*
|
|
27
|
+
* Every release before RP-180 shipped exactly one payload, and that payload
|
|
28
|
+
* is what is now split into `process` + `workflow`. So a manifest with no
|
|
29
|
+
* `layers` key did not choose "core only" — there was no choice to make yet
|
|
30
|
+
* — it installed everything the single array named. Reading the absence as
|
|
31
|
+
* `DEFAULT_LAYERS` (core only) would make the very next `upgrade` treat
|
|
32
|
+
* every workflow file a pre-RP-180 rig has on disk as `retired` and stop
|
|
33
|
+
* managing it: exactly the data-loss direction RP-180's acceptance forbids.
|
|
34
|
+
* Pinned in `packages/cli/test/manifest.test.ts` › "a manifest with no
|
|
35
|
+
* `layers` key parses as though it recorded every layer" and
|
|
36
|
+
* `packages/cli/test/upgrade.test.ts` › "a pre-RP-180 manifest with no
|
|
37
|
+
* `layers` field keeps every workflow file it already has".
|
|
38
|
+
*/
|
|
39
|
+
const LEGACY_LAYERS = ALL_LAYERS;
|
|
40
|
+
const isLayer = (value) => value === 'process' || value === 'workflow';
|
|
18
41
|
export function sha256(data) {
|
|
19
42
|
return createHash('sha256').update(data).digest('hex');
|
|
20
43
|
}
|
|
@@ -24,6 +47,17 @@ function isStringRecord(value) {
|
|
|
24
47
|
!Array.isArray(value) &&
|
|
25
48
|
Object.values(value).every((v) => typeof v === 'string'));
|
|
26
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Manifest keys are both relative paths and user-visible plan text. A path can
|
|
52
|
+
* be lexically harmless to the filesystem while a newline or ANSI escape in
|
|
53
|
+
* it forges the plan a maintainer reviews before approving an upgrade.
|
|
54
|
+
*/
|
|
55
|
+
function isSafeManifestPath(value) {
|
|
56
|
+
return (value !== '' &&
|
|
57
|
+
!path.isAbsolute(value) &&
|
|
58
|
+
!hasControlCharacter(value) &&
|
|
59
|
+
value.split('/').every(isSafeSegment));
|
|
60
|
+
}
|
|
27
61
|
/**
|
|
28
62
|
* A manifest, or `null` when there is nothing trustworthy to read.
|
|
29
63
|
*
|
|
@@ -89,17 +123,35 @@ export function parseManifest(raw) {
|
|
|
89
123
|
return null;
|
|
90
124
|
if (m.stacks.some((s) => !isSafeSubstitutionValue(s)))
|
|
91
125
|
return null;
|
|
92
|
-
|
|
126
|
+
// Optional, like `kept` below — absent is every manifest written before this
|
|
127
|
+
// field existed (resolved to `LEGACY_LAYERS` in the return, not left
|
|
128
|
+
// undefined). Present in a shape this reader does not accept voids the
|
|
129
|
+
// manifest, exactly as `stacks` and `kept` do.
|
|
130
|
+
if (m.layers !== undefined && (!Array.isArray(m.layers) || !m.layers.every(isLayer))) {
|
|
93
131
|
return null;
|
|
132
|
+
}
|
|
133
|
+
if (!isStringRecord(m.files) || Object.keys(m.files).some((rel) => !isSafeManifestPath(rel))) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
94
136
|
// Present in a shape this reader does not accept voids the manifest, exactly
|
|
95
137
|
// as `files` does; absent is every manifest written before the field existed.
|
|
96
|
-
if (m.kept !== undefined &&
|
|
138
|
+
if (m.kept !== undefined &&
|
|
139
|
+
(!isStringRecord(m.kept) || Object.keys(m.kept).some((rel) => !isSafeManifestPath(rel)))) {
|
|
97
140
|
return null;
|
|
141
|
+
}
|
|
98
142
|
return {
|
|
99
143
|
version: m.version,
|
|
100
144
|
kind: m.kind,
|
|
101
145
|
project: { name: project.name, scope: project.scope, region: project.region },
|
|
102
146
|
stacks: [...m.stacks],
|
|
147
|
+
// Deduplicated here, once (RP-180 round 3, security blocker S2): the
|
|
148
|
+
// closed set has exactly two members, so a valid manifest never needs
|
|
149
|
+
// more than two entries, but nothing upstream of this reader bounds the
|
|
150
|
+
// array's length — a committed manifest could otherwise repeat one name
|
|
151
|
+
// thousands of times and make every downstream reader of `.layers` (this
|
|
152
|
+
// module's own callers, `upgrade.ts`, `doctor.mjs`) do unbounded work per
|
|
153
|
+
// entry for no reason.
|
|
154
|
+
layers: m.layers !== undefined ? [...new Set(m.layers)] : [...LEGACY_LAYERS],
|
|
103
155
|
files: { ...m.files },
|
|
104
156
|
...(m.kept !== undefined ? { kept: { ...m.kept } } : {}),
|
|
105
157
|
};
|
|
@@ -133,7 +185,14 @@ export async function readManifest(repoDir) {
|
|
|
133
185
|
}
|
|
134
186
|
}
|
|
135
187
|
export async function writeManifest(repoDir, manifest) {
|
|
136
|
-
const dest =
|
|
188
|
+
const dest = await resolveWritableInside(repoDir, MANIFEST_REL);
|
|
189
|
+
if (dest === null) {
|
|
190
|
+
throw new Error(`Refusing to write "${MANIFEST_REL}" through a symlink or outside ${repoDir}.`);
|
|
191
|
+
}
|
|
137
192
|
await mkdir(path.dirname(dest), { recursive: true });
|
|
138
|
-
await
|
|
193
|
+
const checked = await resolveWritableInside(repoDir, MANIFEST_REL);
|
|
194
|
+
if (checked === null) {
|
|
195
|
+
throw new Error(`Refusing to write "${MANIFEST_REL}" through a symlink or outside ${repoDir}.`);
|
|
196
|
+
}
|
|
197
|
+
await writeFile(checked, serializeManifest(manifest));
|
|
139
198
|
}
|
|
@@ -19,26 +19,3 @@ export function promptConfirm(question, streams) {
|
|
|
19
19
|
});
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Pick a target interactively: by number, by name, or Enter for the default.
|
|
24
|
-
* Anything unrecognised falls back to the default — generation should never
|
|
25
|
-
* dead-end on a typo.
|
|
26
|
-
*/
|
|
27
|
-
export function promptTarget(targets, defaultTarget, streams) {
|
|
28
|
-
if (!streams.isInteractive) {
|
|
29
|
-
return Promise.resolve(defaultTarget);
|
|
30
|
-
}
|
|
31
|
-
const menu = targets
|
|
32
|
-
.map((name, index) => ` ${index + 1}. ${name}${name === defaultTarget ? ' (default)' : ''}`)
|
|
33
|
-
.join('\n');
|
|
34
|
-
const rl = createInterface({ input: streams.input, output: streams.output });
|
|
35
|
-
return new Promise((resolve) => {
|
|
36
|
-
rl.question(`Target:\n${menu}\nChoose [1-${targets.length}]: `, (answer) => {
|
|
37
|
-
rl.close();
|
|
38
|
-
const trimmed = answer.trim();
|
|
39
|
-
const byNumber = targets[Number.parseInt(trimmed, 10) - 1];
|
|
40
|
-
const byName = targets.find((name) => name === trimmed);
|
|
41
|
-
resolve(byName ?? byNumber ?? defaultTarget);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
/**
|
|
3
4
|
* Path safety for values that came from **outside the CLI** — the install
|
|
@@ -46,10 +47,58 @@ export function isSafeSegment(value) {
|
|
|
46
47
|
export function isSafeSubstitutionValue(value) {
|
|
47
48
|
return /^[a-z0-9_][a-z0-9._-]*$/.test(value);
|
|
48
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The most path segments any legitimate `rel` this codebase constructs or
|
|
52
|
+
* tests ever needs.
|
|
53
|
+
*
|
|
54
|
+
* 🔴 No specific number or path is claimed here as evidence for where this is
|
|
55
|
+
* set — a sentence naming today's deepest shipped path would go stale the day
|
|
56
|
+
* a rule moves one directory deeper, and nothing would catch it (the norm
|
|
57
|
+
* `.claude/rules/invariants.md` states under "State the limits — and test
|
|
58
|
+
* them"). What backs this value instead is
|
|
59
|
+
* `packages/cli/test/safe-path.test.ts` › "caps a path at more segments than
|
|
60
|
+
* any path this release's own install set ships, measured not guessed" —
|
|
61
|
+
* it reads the real install set through `initManifest()`, asserts this
|
|
62
|
+
* constant leaves it comfortable headroom, and pins the cap's own refusal
|
|
63
|
+
* behaviour at the boundary. Read that test for the current numbers; this
|
|
64
|
+
* comment does not repeat them so it cannot drift from what the test finds.
|
|
65
|
+
*
|
|
66
|
+
* `rel` is not always this command's own construction, though — a rig
|
|
67
|
+
* manifest is committed input (`uninstall`'s `files`/`kept` keys reach
|
|
68
|
+
* {@link resolveInside} through `onDisk`, one key at a time, before ownership
|
|
69
|
+
* is even checked), so a hostile one can name a key of unbounded segment
|
|
70
|
+
* count. `path.resolve(base, ...segments)` below is a SPREAD over
|
|
71
|
+
* `segments`, and past roughly 65,000–130,000 elements (engine-dependent)
|
|
72
|
+
* that raises an uncaught `RangeError: Maximum call stack size exceeded` —
|
|
73
|
+
* not caught anywhere on this path, so it would surface as a raw stack trace
|
|
74
|
+
* instead of the refusal this function exists to return, and under `--json`
|
|
75
|
+
* as a bare message instead of the promised payload. This is the "no spread
|
|
76
|
+
* of an array whose length is unbounded by input" case named in
|
|
77
|
+
* `.claude/rules/invariants.md`'s fail-open rule; the cap below closes it by
|
|
78
|
+
* refusing before the spread is ever reached.
|
|
79
|
+
*/
|
|
80
|
+
export const MAX_PATH_SEGMENTS = 16;
|
|
81
|
+
/**
|
|
82
|
+
* `rel` has more path segments than {@link MAX_PATH_SEGMENTS} allows.
|
|
83
|
+
* Exported so a caller that needs to say WHICH limit a refusal hit — rather
|
|
84
|
+
* than `resolveInside`'s one undifferentiated `null` — can ask this specific
|
|
85
|
+
* question before calling `resolveInside` at all, without re-implementing the
|
|
86
|
+
* threshold: there is exactly one count-the-segments-and-compare here, and
|
|
87
|
+
* `resolveInside` below calls it rather than repeating it.
|
|
88
|
+
*/
|
|
89
|
+
export function exceedsMaxPathSegments(rel) {
|
|
90
|
+
return rel.split('/').length > MAX_PATH_SEGMENTS;
|
|
91
|
+
}
|
|
49
92
|
/**
|
|
50
93
|
* `rel` resolved under `root`, or `null` when it would land anywhere else —
|
|
51
|
-
* including an absolute path, an empty path,
|
|
52
|
-
* (`/tmp/rig` must not contain `/tmp/rig-evil`)
|
|
94
|
+
* including an absolute path, an empty path, the classic sibling
|
|
95
|
+
* (`/tmp/rig` must not contain `/tmp/rig-evil`), or a segment count past
|
|
96
|
+
* {@link MAX_PATH_SEGMENTS}. The `null` does not say which of these it was;
|
|
97
|
+
* a caller that needs to (`uninstall`'s per-file plan, so it can report a
|
|
98
|
+
* too-deep manifest key honestly instead of through the generic "resolves
|
|
99
|
+
* outside" message) checks {@link exceedsMaxPathSegments} itself, before
|
|
100
|
+
* ever calling this function, rather than trying to reverse-engineer the
|
|
101
|
+
* reason from `null`.
|
|
53
102
|
*
|
|
54
103
|
* This is the containment behind every write an upgrade makes. It is deliberate
|
|
55
104
|
* belt-and-braces: the values that build `rel` are validated where they are
|
|
@@ -58,6 +107,8 @@ export function isSafeSubstitutionValue(value) {
|
|
|
58
107
|
export function resolveInside(root, rel) {
|
|
59
108
|
if (rel === '' || path.isAbsolute(rel))
|
|
60
109
|
return null;
|
|
110
|
+
if (exceedsMaxPathSegments(rel))
|
|
111
|
+
return null;
|
|
61
112
|
const segments = rel.split('/');
|
|
62
113
|
// Refused, not repaired: joining an absolute or `..`-bearing path onto the
|
|
63
114
|
// root would silently turn hostile input into a plausible-looking write.
|
|
@@ -69,3 +120,143 @@ export function resolveInside(root, rel) {
|
|
|
69
120
|
return null;
|
|
70
121
|
return dest.startsWith(base + path.sep) ? dest : null;
|
|
71
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* The one per-segment walk both {@link resolveWritableInside} and
|
|
125
|
+
* {@link resolveReadableInside} build on (RP-22 round 3 advisory: it used to
|
|
126
|
+
* exist twice, nearly identically, in this file). It answers three things a
|
|
127
|
+
* caller can tell apart: every segment existed and was safe (`'complete'`,
|
|
128
|
+
* carrying the FINAL segment's own `lstat` result, since a read-side caller
|
|
129
|
+
* still needs to know its kind — file or directory); some segment did not
|
|
130
|
+
* exist yet, so nothing PAST that point is real (`'stopped-missing'` — a
|
|
131
|
+
* write-side caller treats this as "safe to create", a read-side caller as
|
|
132
|
+
* "absent"); or a segment could not be trusted at all (`'refused'`, with
|
|
133
|
+
* why). It does not itself decide what either outcome MEANS to a caller —
|
|
134
|
+
* that stays in each function, because a missing component means something
|
|
135
|
+
* different to a writer than to a reader, and a behaviour decision belongs
|
|
136
|
+
* at the call site, not in shared plumbing (`.claude/rules/invariants.md`,
|
|
137
|
+
* "One mechanism, one implementation" — the mechanism here is the WALK, not
|
|
138
|
+
* the interpretation of it).
|
|
139
|
+
*/
|
|
140
|
+
async function walkSegments(base, rel) {
|
|
141
|
+
let cursor = base;
|
|
142
|
+
let finalEntry;
|
|
143
|
+
for (const segment of rel.split('/')) {
|
|
144
|
+
cursor = path.join(cursor, segment);
|
|
145
|
+
let resolved;
|
|
146
|
+
try {
|
|
147
|
+
finalEntry = await lstat(cursor);
|
|
148
|
+
if (finalEntry.isSymbolicLink())
|
|
149
|
+
return { outcome: 'refused', reason: 'symlink' };
|
|
150
|
+
// `realpath` stays INSIDE this same try (RP-22 round 4 advisory): the
|
|
151
|
+
// round-2 extraction of this walk from `resolveWritableInside` had
|
|
152
|
+
// moved it outside, so an lstat-succeeds/realpath-throws race (the
|
|
153
|
+
// entry existing for `lstat`, then vanishing or becoming unreadable
|
|
154
|
+
// before `realpath` runs) surfaced as an uncaught exception instead of
|
|
155
|
+
// the same `'refused'` outcome master's own try/catch always gave it.
|
|
156
|
+
// A genuine race is not deterministically testable, so this is
|
|
157
|
+
// restored on the strength of the diff — master's original
|
|
158
|
+
// try/catch scope, read directly — being provably narrower than
|
|
159
|
+
// round 2's, not on a reproduction; the PR body says so.
|
|
160
|
+
resolved = await realpath(cursor);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (error.code === 'ENOENT')
|
|
164
|
+
return { outcome: 'stopped-missing' };
|
|
165
|
+
return {
|
|
166
|
+
outcome: 'refused',
|
|
167
|
+
reason: 'io-error',
|
|
168
|
+
code: error.code,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (resolved !== base && !resolved.startsWith(base + path.sep)) {
|
|
172
|
+
return { outcome: 'refused', reason: 'escapes-root' };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// `rel` is never `''` here — `resolveInside` above already refused that —
|
|
176
|
+
// so `rel.split('/')` has at least one element and the loop always runs at
|
|
177
|
+
// least once: `finalEntry` is always assigned on this path.
|
|
178
|
+
return { outcome: 'complete', finalEntry: finalEntry };
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The read-side counterpart of {@link resolveWritableInside}: walk `rel`
|
|
182
|
+
* under `root` one segment at a time, exactly the same way, but for a
|
|
183
|
+
* caller that only intends to READ what is there — never write, never
|
|
184
|
+
* create. A symlink anywhere in the path is refused, never followed, in
|
|
185
|
+
* EITHER direction: a write-refused symlink that is nonetheless silently
|
|
186
|
+
* READ (a committed `.rig/integrations.json` pointing outside the
|
|
187
|
+
* repository, honoured as if it were the real declaration) is exactly as
|
|
188
|
+
* dangerous as one silently written through, because from S5 on this file's
|
|
189
|
+
* content decides what gets installed (RP-22 round 2 gate finding).
|
|
190
|
+
*
|
|
191
|
+
* Unlike {@link resolveWritableInside}, a missing component is `'absent'`,
|
|
192
|
+
* not a green light — there is nothing to read, and a caller building a
|
|
193
|
+
* fresh declaration from scratch is a different case from one whose read
|
|
194
|
+
* was refused. The final component is additionally required to be the
|
|
195
|
+
* `kind` the caller names, so a directory sitting where a file is expected
|
|
196
|
+
* (or the reverse) is `'unsafe'` rather than surfacing as a raw `EISDIR`/
|
|
197
|
+
* `ENOTDIR` from a subsequent `readFile`/`readdir` call.
|
|
198
|
+
*/
|
|
199
|
+
export async function resolveReadableInside(root, rel, kind) {
|
|
200
|
+
let base;
|
|
201
|
+
try {
|
|
202
|
+
base = await realpath(root);
|
|
203
|
+
if (!(await lstat(base)).isDirectory())
|
|
204
|
+
return { status: 'absent' };
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return { status: 'absent' };
|
|
208
|
+
}
|
|
209
|
+
const dest = resolveInside(base, rel);
|
|
210
|
+
if (dest === null)
|
|
211
|
+
return { status: 'unsafe', reason: 'escapes-root' };
|
|
212
|
+
const walk = await walkSegments(base, rel);
|
|
213
|
+
if (walk.outcome === 'stopped-missing')
|
|
214
|
+
return { status: 'absent' };
|
|
215
|
+
if (walk.outcome === 'refused') {
|
|
216
|
+
if (walk.reason === 'symlink')
|
|
217
|
+
return { status: 'unsafe', reason: 'symlink' };
|
|
218
|
+
if (walk.reason === 'io-error')
|
|
219
|
+
return { status: 'unsafe', reason: 'io-error', code: walk.code };
|
|
220
|
+
return { status: 'unsafe', reason: 'escapes-root' };
|
|
221
|
+
}
|
|
222
|
+
const isRightKind = kind === 'file' ? walk.finalEntry.isFile() : walk.finalEntry.isDirectory();
|
|
223
|
+
if (!isRightKind)
|
|
224
|
+
return { status: 'unsafe', reason: 'wrong-kind' };
|
|
225
|
+
return { status: 'ok', path: dest };
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Resolve a prospective write through the repository's real path and refuse
|
|
229
|
+
* every symlink in the relative path that already exists.
|
|
230
|
+
*
|
|
231
|
+
* Lexical containment is not enough for a writer: `root/.claude` can be a
|
|
232
|
+
* link to another directory, and a link at the final path can be dangling
|
|
233
|
+
* until `writeFile` creates its target. Missing components are allowed because
|
|
234
|
+
* callers create them, then call this function again immediately before the
|
|
235
|
+
* write. The second check makes the newly-created chain evidence too.
|
|
236
|
+
*
|
|
237
|
+
* Observable behaviour is unchanged from before the shared `walkSegments`
|
|
238
|
+
* extraction (RP-22 round 3): a missing component at ANY point in the walk
|
|
239
|
+
* (`'stopped-missing'`) and a fully-walked, fully-safe path (`'complete'`)
|
|
240
|
+
* both still mean "return `dest`" here — only `walkSegments` itself is new,
|
|
241
|
+
* this function's own outward answer for every input is identical to what it
|
|
242
|
+
* was, and `packages/cli/test/safe-path.test.ts`'s existing tests for it are
|
|
243
|
+
* unmodified.
|
|
244
|
+
*/
|
|
245
|
+
export async function resolveWritableInside(root, rel) {
|
|
246
|
+
let base;
|
|
247
|
+
try {
|
|
248
|
+
base = await realpath(root);
|
|
249
|
+
if (!(await lstat(base)).isDirectory())
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const dest = resolveInside(base, rel);
|
|
256
|
+
if (dest === null)
|
|
257
|
+
return null;
|
|
258
|
+
const walk = await walkSegments(base, rel);
|
|
259
|
+
if (walk.outcome === 'refused')
|
|
260
|
+
return null;
|
|
261
|
+
return dest;
|
|
262
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A control or Unicode-format character (RTL override, zero-width joiner, an
|
|
3
|
+
* ESC sequence, a bare CR/LF) inside a string that later reaches a screen a
|
|
4
|
+
* maintainer reads before approving a write — a plan line, a manifest key, a
|
|
5
|
+
* declared integration id. Extracted from `manifest.ts` (RP-22 S1) so a second
|
|
6
|
+
* caller does not duplicate the predicate; the manifest's own behaviour and
|
|
7
|
+
* tests are unchanged by the move (`.claude/rules/invariants.md`, "One
|
|
8
|
+
* mechanism, one implementation").
|
|
9
|
+
*/
|
|
10
|
+
const FORMAT_CHARACTER_PATTERN = /\p{Cf}/u;
|
|
11
|
+
/**
|
|
12
|
+
* Whether one code point (as yielded by spreading a string, so a surrogate
|
|
13
|
+
* pair arrives as a single two-code-unit element) is a Unicode format
|
|
14
|
+
* character (a bidi override, a zero-width joiner, …) or one of the two
|
|
15
|
+
* Unicode line separators (U+2028, U+2029), used by `hasControlCharacter`.
|
|
16
|
+
*/
|
|
17
|
+
export function isFormatOrLineSeparatorCharacter(character) {
|
|
18
|
+
const code = character.charCodeAt(0);
|
|
19
|
+
return code === 0x2028 || code === 0x2029 || FORMAT_CHARACTER_PATTERN.test(character);
|
|
20
|
+
}
|
|
21
|
+
export function hasControlCharacter(value) {
|
|
22
|
+
return [...value].some((character) => {
|
|
23
|
+
const code = character.charCodeAt(0);
|
|
24
|
+
return (code <= 0x1f || (code >= 0x7f && code <= 0x9f) || isFormatOrLineSeparatorCharacter(character));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A type predicate, not a cast: narrows `unknown` to an indexable object
|
|
29
|
+
* without asserting anything. Shared by the integration JSON parsers.
|
|
30
|
+
*/
|
|
31
|
+
export function isPlainObject(value) {
|
|
32
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* One iterative, explicit-worklist pass over a parsed JSON value that answers
|
|
36
|
+
* two questions at once: does anything here carry a control or Unicode-format
|
|
37
|
+
* character, and does the value nest deeper than `maxDepth` below its root.
|
|
38
|
+
*
|
|
39
|
+
* Iterative on purpose. A naive recursive walk over attacker-controlled JSON
|
|
40
|
+
* stack-overflows well inside any reasonable byte cap — `JSON.parse` itself
|
|
41
|
+
* tolerates a depth of 30 000+ — so recursion here would be the one way a
|
|
42
|
+
* caller's "TOTAL, never throws" promise could break
|
|
43
|
+
* (`.claude/rules/invariants.md`, "no recursion over input"). Total work is
|
|
44
|
+
* bounded by the number of JSON tokens in `root`, which the caller's own byte
|
|
45
|
+
* cap must be checked BEFORE this ever runs.
|
|
46
|
+
*
|
|
47
|
+
* The caller selects the depth bound for its accepted schema.
|
|
48
|
+
*/
|
|
49
|
+
export function scanForDepthAndControlChars(root, maxDepth) {
|
|
50
|
+
const stack = [{ value: root, depth: 0 }];
|
|
51
|
+
let hasControlChar = false;
|
|
52
|
+
while (stack.length > 0) {
|
|
53
|
+
const next = stack.pop();
|
|
54
|
+
if (next === undefined)
|
|
55
|
+
break; // guarded by the loop condition; stated for the type checker
|
|
56
|
+
const { value, depth } = next;
|
|
57
|
+
if (depth > maxDepth)
|
|
58
|
+
return { tooDeep: true, hasControlChar };
|
|
59
|
+
if (typeof value === 'string') {
|
|
60
|
+
if (hasControlCharacter(value))
|
|
61
|
+
hasControlChar = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
for (const item of value)
|
|
66
|
+
stack.push({ value: item, depth: depth + 1 });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (typeof value === 'object' && value !== null) {
|
|
70
|
+
for (const [key, child] of Object.entries(value)) {
|
|
71
|
+
if (hasControlCharacter(key))
|
|
72
|
+
hasControlChar = true;
|
|
73
|
+
stack.push({ value: child, depth: depth + 1 });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { tooDeep: false, hasControlChar };
|
|
78
|
+
}
|
|
@@ -1,54 +1,3 @@
|
|
|
1
1
|
export function substituteContent(content, ctx) {
|
|
2
|
-
return content
|
|
3
|
-
.replaceAll('__PROJECT_NAME__', ctx.projectName)
|
|
4
|
-
.replaceAll('__PROJECT_SCOPE__', ctx.projectScope)
|
|
5
|
-
.replaceAll('__REGION__', ctx.region)
|
|
6
|
-
.replaceAll('@app/', `@${ctx.projectScope}/`);
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* The inverse of {@link substituteContent}, used **only to recognise** an
|
|
10
|
-
* installed file as a released version of its template: the released bytes
|
|
11
|
-
* carry tokens, the installed bytes carry the project's own name, and without
|
|
12
|
-
* this every token-carrying file (the kill switch, the loop skill, `CLAUDE.md`)
|
|
13
|
-
* would be a permanent conflict on every rig.
|
|
14
|
-
*
|
|
15
|
-
* 🔴 Limits, and both of them fail in the safe direction — an unrecognised file
|
|
16
|
-
* is reported and left alone, never overwritten:
|
|
17
|
-
*
|
|
18
|
-
* - `__PROJECT_SCOPE__` is not reversed. It substitutes to the same string as
|
|
19
|
-
* `__PROJECT_NAME__`, so the two are indistinguishable after the fact; the
|
|
20
|
-
* agent-os layer (the only layer an upgrade touches) uses neither the scope
|
|
21
|
-
* token nor `@app/` in prose, which a template test pins.
|
|
22
|
-
* - A template that contains the project's name as a *literal* reverses into a
|
|
23
|
-
* token that was never there. That costs nothing on its own — recognition
|
|
24
|
-
* offers the untouched bytes as a candidate too, and those still match. It
|
|
25
|
-
* bites only on a file carrying **both** a token and the literal, which then
|
|
26
|
-
* reads as a conflict for that one project.
|
|
27
|
-
*/
|
|
28
|
-
export function detokenizeContent(content, ctx) {
|
|
29
|
-
let out = content;
|
|
30
|
-
// scope first: reversing the name first would rewrite `@name/` into
|
|
31
|
-
// `@__PROJECT_NAME__/` and the scope form could never match afterwards
|
|
32
|
-
if (ctx.projectScope !== '')
|
|
33
|
-
out = out.replaceAll(`@${ctx.projectScope}/`, '@app/');
|
|
34
|
-
if (ctx.region !== '')
|
|
35
|
-
out = out.replaceAll(ctx.region, '__REGION__');
|
|
36
|
-
if (ctx.projectName !== '')
|
|
37
|
-
out = out.replaceAll(ctx.projectName, '__PROJECT_NAME__');
|
|
38
|
-
return out;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Files that must exist in the generated project under a dotted name, but are
|
|
42
|
-
* stored un-dotted in the template because `npm publish` strips the dotted
|
|
43
|
-
* original from tarballs (the create-react-app `gitignore` trick).
|
|
44
|
-
*/
|
|
45
|
-
const UNDOTTED_NAMES = {
|
|
46
|
-
gitignore: '.gitignore',
|
|
47
|
-
};
|
|
48
|
-
export function substituteFileName(name, ctx) {
|
|
49
|
-
const substituted = name
|
|
50
|
-
.replaceAll('__PROJECT_NAME__', ctx.projectName)
|
|
51
|
-
.replaceAll('__PROJECT_SCOPE__', ctx.projectScope)
|
|
52
|
-
.replaceAll('__REGION__', ctx.region);
|
|
53
|
-
return UNDOTTED_NAMES[substituted] ?? substituted;
|
|
2
|
+
return content.replaceAll('__PROJECT_NAME__', ctx.projectName);
|
|
54
3
|
}
|
|
@@ -28,18 +28,18 @@ export async function collectGovernance(projectDir) {
|
|
|
28
28
|
// directory, and counting a config file as an enforced hook would make the
|
|
29
29
|
// screen overstate the one thing this tool actually sells.
|
|
30
30
|
hooks: (await names(path.join(claude, 'hooks'), /\.mjs$/, /\.mjs$/)).map(
|
|
31
|
-
// guard-
|
|
31
|
+
// guard-secret-file → "secret file": the mechanism, not the filename
|
|
32
32
|
(hook) => HOOK_LABELS[hook] ?? hook.replace(/^(guard|block)-/, '').replaceAll('-', ' ')),
|
|
33
33
|
skills: await names(path.join(claude, 'skills'), /$^/),
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
-
export function renderSummary(projectName,
|
|
36
|
+
export function renderSummary(projectName, dirArg, summary, p) {
|
|
37
37
|
const row = (label, count, detail, note = '') => {
|
|
38
38
|
const tag = note ? ` ${note}` : '';
|
|
39
39
|
return ` ${label.padEnd(8)}${String(count).padEnd(3)}${tag.padEnd(note ? 11 : 0)} ${p.dim(detail)}`;
|
|
40
40
|
};
|
|
41
41
|
return [
|
|
42
|
-
`Created ${p.accent(projectName)}
|
|
42
|
+
`Created ${p.accent(projectName)}`,
|
|
43
43
|
'',
|
|
44
44
|
row('Rules', summary.rules.length, '.claude/rules/'),
|
|
45
45
|
row('Agents', summary.agents.length, summary.agents.join(', ')),
|
|
@@ -48,8 +48,8 @@ export function renderSummary(projectName, target, dirArg, summary, p) {
|
|
|
48
48
|
'',
|
|
49
49
|
'Next',
|
|
50
50
|
p.dim(` cd ${dirArg}`),
|
|
51
|
-
p.dim('
|
|
52
|
-
p.dim('
|
|
51
|
+
p.dim(' codex'),
|
|
52
|
+
p.dim(' claude'),
|
|
53
53
|
'',
|
|
54
54
|
].join('\n');
|
|
55
55
|
}
|
|
@@ -10,20 +10,10 @@ import { fileURLToPath } from 'node:url';
|
|
|
10
10
|
export function templatesRoot() {
|
|
11
11
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'templates');
|
|
12
12
|
}
|
|
13
|
-
export function skeletonDir(skeleton) {
|
|
14
|
-
return path.join(templatesRoot(), 'skeleton', skeleton);
|
|
15
|
-
}
|
|
16
|
-
export function agentOsUniversalDir() {
|
|
17
|
-
return path.join(templatesRoot(), 'agent-os', 'universal');
|
|
18
|
-
}
|
|
19
|
-
export function agentOsStackDir(stack) {
|
|
20
|
-
return path.join(templatesRoot(), 'agent-os', 'stack', stack);
|
|
21
|
-
}
|
|
22
13
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* shape the generator knows nothing about. `create` never reads this directory.
|
|
14
|
+
* The one payload this package ships (RP-177 retired the per-target skeleton
|
|
15
|
+
* and the per-stack overlays that used to sit alongside it).
|
|
26
16
|
*/
|
|
27
|
-
export function
|
|
28
|
-
return path.join(templatesRoot(), 'agent-os', '
|
|
17
|
+
export function agentOsUniversalDir() {
|
|
18
|
+
return path.join(templatesRoot(), 'agent-os', 'universal');
|
|
29
19
|
}
|
package/scripts/prepare.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// (`npx github:<user>/create-agent-rig`). It must therefore work with only
|
|
3
3
|
// the root devDependencies present and no pnpm available.
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { existsSync } from 'node:fs';
|
|
5
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -34,17 +34,15 @@ export const gitConfigEnv = (env = process.env) => {
|
|
|
34
34
|
return sanitised;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
37
|
+
/**
|
|
38
|
+
* Build from a clean output directory. TypeScript overwrites emitted files,
|
|
39
|
+
* but it does not remove JavaScript whose source disappeared; without this
|
|
40
|
+
* boundary `npm pack` can publish retired commands from an older checkout.
|
|
41
|
+
*/
|
|
42
|
+
export function buildCli() {
|
|
43
|
+
const dist = path.join(root, 'packages/cli/dist');
|
|
44
|
+
rmSync(dist, { recursive: true, force: true });
|
|
46
45
|
|
|
47
|
-
// 2. Build the CLI so the `bin` entry exists (required for git/tarball installs).
|
|
48
46
|
const require = createRequire(import.meta.url);
|
|
49
47
|
const tscPath = path.join(
|
|
50
48
|
path.dirname(require.resolve('typescript/package.json')),
|
|
@@ -56,7 +54,25 @@ function main() {
|
|
|
56
54
|
[tscPath, '-p', path.join(root, 'packages/cli/tsconfig.build.json')],
|
|
57
55
|
{ cwd: root, stdio: 'inherit' },
|
|
58
56
|
);
|
|
59
|
-
|
|
57
|
+
return result.status ?? 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function main() {
|
|
61
|
+
// 1. Wire up the pre-commit hook when working inside the git checkout.
|
|
62
|
+
if (
|
|
63
|
+
!process.argv.includes('--build-only') &&
|
|
64
|
+
!process.env.CI &&
|
|
65
|
+
existsSync(path.join(root, '.git'))
|
|
66
|
+
) {
|
|
67
|
+
spawnSync('git', ['config', 'core.hooksPath', '.husky'], {
|
|
68
|
+
cwd: root,
|
|
69
|
+
env: gitConfigEnv(),
|
|
70
|
+
stdio: 'inherit',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2. Build the CLI so the `bin` entry exists (required for git/tarball installs).
|
|
75
|
+
process.exit(buildCli());
|
|
60
76
|
}
|
|
61
77
|
|
|
62
78
|
// Run only when executed, never when imported: a test that imports this module
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
"claude": { "model": "claude-sonnet-5", "effort": "high" },
|
|
13
13
|
"codex": { "model": "gpt-5.6-terra", "effort": "high" }
|
|
14
14
|
},
|
|
15
|
+
"implementation-agent": {
|
|
16
|
+
"claude": { "model": "claude-sonnet-5", "effort": "high" },
|
|
17
|
+
"codex": { "model": "gpt-5.6-terra", "effort": "high" }
|
|
18
|
+
},
|
|
15
19
|
"prose-reviewer": {
|
|
16
20
|
"claude": { "model": "claude-sonnet-5", "effort": "high" },
|
|
17
21
|
"codex": { "model": "gpt-5.6-terra", "effort": "high" }
|
|
@@ -23,10 +27,6 @@
|
|
|
23
27
|
"security-scanner": {
|
|
24
28
|
"claude": { "model": "claude-opus-5", "effort": "high" },
|
|
25
29
|
"codex": { "model": "gpt-5.6-sol", "effort": "high" }
|
|
26
|
-
},
|
|
27
|
-
"cdk-diff-reviewer": {
|
|
28
|
-
"claude": { "model": "claude-opus-5", "effort": "high" },
|
|
29
|
-
"codex": { "model": "gpt-5.6-sol", "effort": "high" }
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
}
|