@cluesmith/codev 3.3.4 → 3.3.6
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/dashboard-dist/assets/index-BjcqV9Vv.css +32 -0
- package/dashboard-dist/assets/index-b7GnZV2i.js +220 -0
- package/dashboard-dist/assets/index-b7GnZV2i.js.map +1 -0
- package/dashboard-dist/index.html +2 -2
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +50 -17
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/architect.d.ts.map +1 -1
- package/dist/agent-farm/commands/architect.js +5 -1
- package/dist/agent-farm/commands/architect.js.map +1 -1
- package/dist/agent-farm/commands/attach.d.ts +1 -1
- package/dist/agent-farm/commands/attach.d.ts.map +1 -1
- package/dist/agent-farm/commands/cleanup.d.ts +1 -1
- package/dist/agent-farm/commands/cleanup.d.ts.map +1 -1
- package/dist/agent-farm/commands/cleanup.js +2 -1
- package/dist/agent-farm/commands/cleanup.js.map +1 -1
- package/dist/agent-farm/commands/decisions.d.ts +25 -0
- package/dist/agent-farm/commands/decisions.d.ts.map +1 -0
- package/dist/agent-farm/commands/decisions.js +54 -0
- package/dist/agent-farm/commands/decisions.js.map +1 -0
- package/dist/agent-farm/commands/ide.d.ts +32 -3
- package/dist/agent-farm/commands/ide.d.ts.map +1 -1
- package/dist/agent-farm/commands/ide.js +180 -19
- package/dist/agent-farm/commands/ide.js.map +1 -1
- package/dist/agent-farm/commands/reset/context.d.ts +4 -2
- package/dist/agent-farm/commands/reset/context.d.ts.map +1 -1
- package/dist/agent-farm/commands/reset/context.js +12 -4
- package/dist/agent-farm/commands/reset/context.js.map +1 -1
- package/dist/agent-farm/commands/send.d.ts +3 -2
- package/dist/agent-farm/commands/send.d.ts.map +1 -1
- package/dist/agent-farm/commands/send.js +35 -21
- package/dist/agent-farm/commands/send.js.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.d.ts +7 -0
- package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.js +63 -20
- package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
- package/dist/agent-farm/commands/spawn.d.ts +17 -0
- package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +41 -19
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/lib/builder-lookup.d.ts +3 -2
- package/dist/agent-farm/lib/builder-lookup.d.ts.map +1 -1
- package/dist/agent-farm/lib/builder-lookup.js +24 -9
- package/dist/agent-farm/lib/builder-lookup.js.map +1 -1
- package/dist/agent-farm/lib/ide-extensions.d.ts +82 -0
- package/dist/agent-farm/lib/ide-extensions.d.ts.map +1 -0
- package/dist/agent-farm/lib/ide-extensions.js +179 -0
- package/dist/agent-farm/lib/ide-extensions.js.map +1 -0
- package/dist/agent-farm/lib/ide-record.d.ts +6 -1
- package/dist/agent-farm/lib/ide-record.d.ts.map +1 -1
- package/dist/agent-farm/lib/ide-record.js +8 -1
- package/dist/agent-farm/lib/ide-record.js.map +1 -1
- package/dist/agent-farm/lib/ide-version.d.ts +97 -0
- package/dist/agent-farm/lib/ide-version.d.ts.map +1 -0
- package/dist/agent-farm/lib/ide-version.js +224 -0
- package/dist/agent-farm/lib/ide-version.js.map +1 -0
- package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.js +7 -3
- package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
- package/dist/agent-farm/servers/agent-state/broadcast.d.ts +19 -0
- package/dist/agent-farm/servers/agent-state/broadcast.d.ts.map +1 -0
- package/dist/agent-farm/servers/agent-state/broadcast.js +30 -0
- package/dist/agent-farm/servers/agent-state/broadcast.js.map +1 -0
- package/dist/agent-farm/servers/agent-state/identity.d.ts +50 -0
- package/dist/agent-farm/servers/agent-state/identity.d.ts.map +1 -0
- package/dist/agent-farm/servers/agent-state/identity.js +120 -0
- package/dist/agent-farm/servers/agent-state/identity.js.map +1 -0
- package/dist/agent-farm/servers/agent-state/liveness.d.ts +107 -0
- package/dist/agent-farm/servers/agent-state/liveness.d.ts.map +1 -0
- package/dist/agent-farm/servers/agent-state/liveness.js +224 -0
- package/dist/agent-farm/servers/agent-state/liveness.js.map +1 -0
- package/dist/agent-farm/servers/agent-state/route.d.ts +38 -0
- package/dist/agent-farm/servers/agent-state/route.d.ts.map +1 -0
- package/dist/agent-farm/servers/agent-state/route.js +170 -0
- package/dist/agent-farm/servers/agent-state/route.js.map +1 -0
- package/dist/agent-farm/servers/agent-state/serve.d.ts +31 -0
- package/dist/agent-farm/servers/agent-state/serve.d.ts.map +1 -0
- package/dist/agent-farm/servers/agent-state/serve.js +49 -0
- package/dist/agent-farm/servers/agent-state/serve.js.map +1 -0
- package/dist/agent-farm/servers/agent-state/store.d.ts +76 -0
- package/dist/agent-farm/servers/agent-state/store.d.ts.map +1 -0
- package/dist/agent-farm/servers/agent-state/store.js +157 -0
- package/dist/agent-farm/servers/agent-state/store.js.map +1 -0
- package/dist/agent-farm/servers/decisions/file.d.ts +50 -0
- package/dist/agent-farm/servers/decisions/file.d.ts.map +1 -0
- package/dist/agent-farm/servers/decisions/file.js +146 -0
- package/dist/agent-farm/servers/decisions/file.js.map +1 -0
- package/dist/agent-farm/servers/decisions/grammar.d.ts +36 -0
- package/dist/agent-farm/servers/decisions/grammar.d.ts.map +1 -0
- package/dist/agent-farm/servers/decisions/grammar.js +109 -0
- package/dist/agent-farm/servers/decisions/grammar.js.map +1 -0
- package/dist/agent-farm/servers/decisions/notice.d.ts +11 -0
- package/dist/agent-farm/servers/decisions/notice.d.ts.map +1 -0
- package/dist/agent-farm/servers/decisions/notice.js +18 -0
- package/dist/agent-farm/servers/decisions/notice.js.map +1 -0
- package/dist/agent-farm/servers/decisions/parse.d.ts +36 -0
- package/dist/agent-farm/servers/decisions/parse.d.ts.map +1 -0
- package/dist/agent-farm/servers/decisions/parse.js +338 -0
- package/dist/agent-farm/servers/decisions/parse.js.map +1 -0
- package/dist/agent-farm/servers/decisions/routes.d.ts +17 -0
- package/dist/agent-farm/servers/decisions/routes.d.ts.map +1 -0
- package/dist/agent-farm/servers/decisions/routes.js +160 -0
- package/dist/agent-farm/servers/decisions/routes.js.map +1 -0
- package/dist/agent-farm/servers/decisions/splice.d.ts +26 -0
- package/dist/agent-farm/servers/decisions/splice.d.ts.map +1 -0
- package/dist/agent-farm/servers/decisions/splice.js +120 -0
- package/dist/agent-farm/servers/decisions/splice.js.map +1 -0
- package/dist/agent-farm/servers/decisions/stamp.d.ts +8 -0
- package/dist/agent-farm/servers/decisions/stamp.d.ts.map +1 -0
- package/dist/agent-farm/servers/decisions/stamp.js +195 -0
- package/dist/agent-farm/servers/decisions/stamp.js.map +1 -0
- package/dist/agent-farm/servers/decisions/template.d.ts +2 -0
- package/dist/agent-farm/servers/decisions/template.d.ts.map +1 -0
- package/dist/agent-farm/servers/decisions/template.js +15 -0
- package/dist/agent-farm/servers/decisions/template.js.map +1 -0
- package/dist/agent-farm/servers/ide-server.d.ts +28 -8
- package/dist/agent-farm/servers/ide-server.d.ts.map +1 -1
- package/dist/agent-farm/servers/ide-server.js +147 -50
- package/dist/agent-farm/servers/ide-server.js.map +1 -1
- package/dist/agent-farm/servers/overview.d.ts +31 -4
- package/dist/agent-farm/servers/overview.d.ts.map +1 -1
- package/dist/agent-farm/servers/overview.js +235 -55
- package/dist/agent-farm/servers/overview.js.map +1 -1
- package/dist/agent-farm/servers/tower-instances.d.ts +14 -1
- package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-instances.js +51 -9
- package/dist/agent-farm/servers/tower-instances.js.map +1 -1
- package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-routes.js +113 -14
- package/dist/agent-farm/servers/tower-routes.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +35 -1
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.d.ts +25 -0
- package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.js +128 -4
- package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
- package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-utils.js +18 -5
- package/dist/agent-farm/servers/tower-utils.js.map +1 -1
- package/dist/agent-farm/utils/builder-worktree-identity.d.ts +39 -0
- package/dist/agent-farm/utils/builder-worktree-identity.d.ts.map +1 -0
- package/dist/agent-farm/utils/builder-worktree-identity.js +42 -0
- package/dist/agent-farm/utils/builder-worktree-identity.js.map +1 -0
- package/dist/agent-farm/utils/harness.d.ts +71 -0
- package/dist/agent-farm/utils/harness.d.ts.map +1 -1
- package/dist/agent-farm/utils/harness.js +153 -0
- package/dist/agent-farm/utils/harness.js.map +1 -1
- package/dist/agent-farm/utils/lifecycle-reporter.d.ts +79 -0
- package/dist/agent-farm/utils/lifecycle-reporter.d.ts.map +1 -0
- package/dist/agent-farm/utils/lifecycle-reporter.js +347 -0
- package/dist/agent-farm/utils/lifecycle-reporter.js.map +1 -0
- package/dist/agent-farm/utils/message-format.d.ts +8 -0
- package/dist/agent-farm/utils/message-format.d.ts.map +1 -1
- package/dist/agent-farm/utils/message-format.js +16 -0
- package/dist/agent-farm/utils/message-format.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +17 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts +16 -2
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +67 -23
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/bootstrap-docs.d.ts +132 -0
- package/dist/commands/bootstrap-docs.d.ts.map +1 -0
- package/dist/commands/bootstrap-docs.js +725 -0
- package/dist/commands/bootstrap-docs.js.map +1 -0
- package/dist/commands/consult/index.d.ts +20 -1
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +70 -3
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/consult/tree-tripwire.d.ts +2 -75
- package/dist/commands/consult/tree-tripwire.d.ts.map +1 -1
- package/dist/commands/consult/tree-tripwire.js +7 -184
- package/dist/commands/consult/tree-tripwire.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +36 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/porch/checks.d.ts +7 -1
- package/dist/commands/porch/checks.d.ts.map +1 -1
- package/dist/commands/porch/checks.js +77 -14
- package/dist/commands/porch/checks.js.map +1 -1
- package/dist/commands/porch/index.d.ts +38 -1
- package/dist/commands/porch/index.d.ts.map +1 -1
- package/dist/commands/porch/index.js +79 -43
- package/dist/commands/porch/index.js.map +1 -1
- package/dist/commands/porch/protocol.d.ts +4 -3
- package/dist/commands/porch/protocol.d.ts.map +1 -1
- package/dist/commands/porch/protocol.js +32 -5
- package/dist/commands/porch/protocol.js.map +1 -1
- package/dist/commands/porch/state.d.ts +0 -4
- package/dist/commands/porch/state.d.ts.map +1 -1
- package/dist/commands/porch/state.js +23 -10
- package/dist/commands/porch/state.js.map +1 -1
- package/dist/commands/porch/types.d.ts +2 -0
- package/dist/commands/porch/types.d.ts.map +1 -1
- package/dist/commands/update.d.ts +5 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +73 -18
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/agent-env.d.ts +31 -1
- package/dist/lib/agent-env.d.ts.map +1 -1
- package/dist/lib/agent-env.js +38 -1
- package/dist/lib/agent-env.js.map +1 -1
- package/dist/lib/agent-process.d.ts +23 -0
- package/dist/lib/agent-process.d.ts.map +1 -0
- package/dist/lib/agent-process.js +24 -0
- package/dist/lib/agent-process.js.map +1 -0
- package/dist/lib/config.d.ts +8 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/forge-contracts.d.ts +9 -3
- package/dist/lib/forge-contracts.d.ts.map +1 -1
- package/dist/lib/forge-contracts.js.map +1 -1
- package/dist/lib/forge.d.ts +8 -5
- package/dist/lib/forge.d.ts.map +1 -1
- package/dist/lib/forge.js +57 -1
- package/dist/lib/forge.js.map +1 -1
- package/dist/lib/github.d.ts.map +1 -1
- package/dist/lib/github.js +38 -0
- package/dist/lib/github.js.map +1 -1
- package/dist/lib/governance-bootstrap.d.ts +155 -0
- package/dist/lib/governance-bootstrap.d.ts.map +1 -0
- package/dist/lib/governance-bootstrap.js +469 -0
- package/dist/lib/governance-bootstrap.js.map +1 -0
- package/dist/lib/issue-id.d.ts +34 -0
- package/dist/lib/issue-id.d.ts.map +1 -0
- package/dist/lib/issue-id.js +61 -0
- package/dist/lib/issue-id.js.map +1 -0
- package/dist/lib/scaffold.d.ts +4 -0
- package/dist/lib/scaffold.d.ts.map +1 -1
- package/dist/lib/scaffold.js +29 -5
- package/dist/lib/scaffold.js.map +1 -1
- package/dist/lib/shipped-skill-hashes.d.ts +10 -0
- package/dist/lib/shipped-skill-hashes.d.ts.map +1 -0
- package/dist/lib/shipped-skill-hashes.js +197 -0
- package/dist/lib/shipped-skill-hashes.js.map +1 -0
- package/dist/lib/skill-refresh.d.ts +90 -0
- package/dist/lib/skill-refresh.d.ts.map +1 -0
- package/dist/lib/skill-refresh.js +267 -0
- package/dist/lib/skill-refresh.js.map +1 -0
- package/dist/lib/tree-snapshot.d.ts +172 -0
- package/dist/lib/tree-snapshot.d.ts.map +1 -0
- package/dist/lib/tree-snapshot.js +393 -0
- package/dist/lib/tree-snapshot.js.map +1 -0
- package/dist/terminal/pty-session.d.ts +5 -0
- package/dist/terminal/pty-session.d.ts.map +1 -1
- package/dist/terminal/pty-session.js +7 -0
- package/dist/terminal/pty-session.js.map +1 -1
- package/ide-extension/codev-vscode.vsix +0 -0
- package/ide-extension/manifest.json +7 -0
- package/package.json +8 -6
- package/scripts/forge/jira/_lib.sh +113 -0
- package/scripts/forge/jira/auth-status.sh +29 -0
- package/scripts/forge/jira/issue-comment.sh +23 -0
- package/scripts/forge/jira/issue-list.sh +32 -0
- package/scripts/forge/jira/issue-search.sh +40 -0
- package/scripts/forge/jira/issue-view.sh +41 -0
- package/scripts/forge/jira/recently-closed.sh +81 -0
- package/scripts/forge/jira/user-identity.sh +18 -0
- package/skeleton/.claude/skills/afx/SKILL.md +51 -0
- package/skeleton/.claude/skills/arch-init/SKILL.md +56 -9
- package/skeleton/.claude/skills/arch-save/SKILL.md +100 -9
- package/skeleton/.claude/skills/codev/SKILL.md +18 -2
- package/skeleton/.claude/skills/consult/SKILL.md +9 -0
- package/skeleton/.claude/skills/porch/SKILL.md +18 -0
- package/skeleton/.claude/skills/update-arch-docs/SKILL.md +28 -2
- package/skeleton/.codex/skills/afx/SKILL.md +51 -0
- package/skeleton/.codex/skills/arch-init/SKILL.md +56 -9
- package/skeleton/.codex/skills/arch-save/SKILL.md +100 -9
- package/skeleton/.codex/skills/codev/SKILL.md +18 -2
- package/skeleton/.codex/skills/consult/SKILL.md +9 -0
- package/skeleton/.codex/skills/porch/SKILL.md +18 -0
- package/skeleton/.codex/skills/update-arch-docs/SKILL.md +28 -2
- package/skeleton/protocol-schema.json +6 -0
- package/skeleton/protocols/maintain/protocol.md +5 -0
- package/skeleton/protocols/pir/prompts/review.md +0 -1
- package/skeleton/protocols/protocol-schema.json +6 -0
- package/skeleton/resources/commands/agent-farm.md +156 -0
- package/skeleton/resources/commands/codev.md +47 -0
- package/skeleton/resources/commands/consult.md +9 -2
- package/skeleton/resources/commands/overview.md +1 -0
- package/skeleton/resources/decision-protocol.md +215 -0
- package/skeleton/resources/workflow-reference.md +12 -0
- package/skeleton/roles/architect.md +6 -0
- package/skeleton/templates/bootstrap-docs-prompt.md +16 -0
- package/skeleton/templates/bootstrap-docs-review-prompt.md +19 -0
- package/skeleton/templates/cheatsheet.md +2 -1
- package/dashboard-dist/assets/index-DZf6pNPE.js +0 -199
- package/dashboard-dist/assets/index-DZf6pNPE.js.map +0 -1
- package/dashboard-dist/assets/index-DnYLy5C_.css +0 -32
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `codev bootstrap-docs` (Spec 1797): draft the four governance docs from an existing
|
|
3
|
+
* codebase, in place of the starter placeholders `codev adopt` leaves behind.
|
|
4
|
+
*
|
|
5
|
+
* The CLI owns everything deterministic: which files are still untouched starters, whether
|
|
6
|
+
* there is anything to document, the launch, the before/after change report, validation of
|
|
7
|
+
* the drafts and the summary. The agent owns only the writing, under the rules of the
|
|
8
|
+
* `update-arch-docs` skill's bootstrap mode: the project's installed copy when it matches
|
|
9
|
+
* the package (#1811), the packaged text inlined into the prompt otherwise.
|
|
10
|
+
*
|
|
11
|
+
* An interactive agent cannot be sandboxed from here, so the guarantee is detect-and-report:
|
|
12
|
+
* anything that moved outside the eligible files is named and the exit code is non-zero.
|
|
13
|
+
* Nothing is ever reverted, staged or committed; the drafts are the human's to review.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import { spawn } from 'node:child_process';
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
import { spawnAgentProcess } from '../lib/agent-process.js';
|
|
20
|
+
import { confirm } from '../lib/cli-prompts.js';
|
|
21
|
+
import { loadConfig } from '../lib/config.js';
|
|
22
|
+
import { getTemplatesDir } from '../lib/templates.js';
|
|
23
|
+
import { GOVERNANCE_FILES, bootstrapEligibleFiles, classifyProjectFile, classifyProjectGovernance, irregularAncestor, governancePathKind, isNonEmptyRepo, knownStarterTexts, topLevelHeadings, validateColdFile, validateHotFile, HOT_CAP_MAP_TOPICS, } from '../lib/governance-bootstrap.js';
|
|
24
|
+
import { CONSULT_OWNED_PREFIXES, FILESYSTEM_WALK_LIMIT, FILESYSTEM_WALK_SKIPPED, diffRepositoryIdentity, diffSnapshotEntries, escapeControlChars, hashPath, readIndexFingerprint, snapshotFilesystem, snapshotTree, } from '../lib/tree-snapshot.js';
|
|
25
|
+
import { resolveInteractiveTaskLauncher, } from '../agent-farm/utils/harness.js';
|
|
26
|
+
import { classifySkill, skillRelDir } from '../lib/skill-refresh.js';
|
|
27
|
+
import { SKILL_PROVIDERS } from '../lib/scaffold.js';
|
|
28
|
+
/** Where the rendered prompt is saved, for the manual fallback and for the record. */
|
|
29
|
+
export const PROMPT_FILE = '.consult/bootstrap-docs/prompt.md';
|
|
30
|
+
/** Where the draft review's prompt and each reviewer's feedback are written. */
|
|
31
|
+
export const REVIEW_DIR = '.consult/bootstrap-docs';
|
|
32
|
+
export const REVIEW_PROMPT_FILE = `${REVIEW_DIR}/review-prompt.md`;
|
|
33
|
+
/** The three reviewers of the drafts, run in parallel. */
|
|
34
|
+
export const REVIEW_MODELS = ['gemini', 'codex', 'claude'];
|
|
35
|
+
/**
|
|
36
|
+
* Largest prompt passed as a command-line argument. Linux caps one argument at 128 KB
|
|
37
|
+
* (MAX_ARG_STRLEN); above this the prompt goes to the manual fallback instead of failing
|
|
38
|
+
* the spawn with E2BIG.
|
|
39
|
+
*/
|
|
40
|
+
export const MAX_PROMPT_BYTES = 100_000;
|
|
41
|
+
/**
|
|
42
|
+
* Paths the agent harness itself writes during an ordinary session. Still reported (the spec
|
|
43
|
+
* allows no exemption beyond the eligible files and `.consult/`), but annotated so the human
|
|
44
|
+
* can judge them quickly.
|
|
45
|
+
*/
|
|
46
|
+
const HARNESS_SCRATCH_PATHS = {
|
|
47
|
+
'.claude/settings.local.json': 'written by the agent harness itself, usually benign',
|
|
48
|
+
};
|
|
49
|
+
function governancePath(name) {
|
|
50
|
+
return `codev/resources/${name}`;
|
|
51
|
+
}
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// Prompt
|
|
54
|
+
// =============================================================================
|
|
55
|
+
/**
|
|
56
|
+
* Fill `{{NAME}}` placeholders in one pass, so text inserted for one placeholder (the skill
|
|
57
|
+
* text in particular) is never itself scanned for placeholders.
|
|
58
|
+
*/
|
|
59
|
+
export function renderPrompt(template, values) {
|
|
60
|
+
return template.replace(/\{\{([A-Z_]+)\}\}/g, (match, key) => values[key] ?? match);
|
|
61
|
+
}
|
|
62
|
+
function fileList(names) {
|
|
63
|
+
if (names.length === 0)
|
|
64
|
+
return '(none)';
|
|
65
|
+
return names.map(name => `- \`${governancePath(name)}\``).join('\n');
|
|
66
|
+
}
|
|
67
|
+
const BOOTSTRAP_SKILL = 'update-arch-docs';
|
|
68
|
+
const INLINE_REASONS = {
|
|
69
|
+
missing: 'is missing',
|
|
70
|
+
shipped: 'is an older version (`codev update` refreshes it)',
|
|
71
|
+
edited: 'has local edits',
|
|
72
|
+
'not-regular': 'is a symlink, holds one, is unreadable, or is not a directory',
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Run the project's installed skill by name when the launching harness loads it and the copy
|
|
76
|
+
* is identical to the package's, so the rules the agent follows are exactly the ones the
|
|
77
|
+
* drafts are validated against. Anything else gets the packaged text inlined.
|
|
78
|
+
*/
|
|
79
|
+
export function chooseSkillSource(root, skeletonDir, harnessName) {
|
|
80
|
+
const provider = SKILL_PROVIDERS.find(name => name === harnessName);
|
|
81
|
+
if (!provider)
|
|
82
|
+
return { kind: 'inline', reason: 'the agent that runs this prompt may not load project skills' };
|
|
83
|
+
const dir = skillRelDir(provider, BOOTSTRAP_SKILL);
|
|
84
|
+
const state = classifySkill(path.join(root, dir), path.join(skeletonDir, dir), dir);
|
|
85
|
+
if (state === 'current')
|
|
86
|
+
return { kind: 'installed', dir };
|
|
87
|
+
return { kind: 'inline', reason: `the installed copy at \`${dir}/\` ${INLINE_REASONS[state]}`, provider };
|
|
88
|
+
}
|
|
89
|
+
export function describeSkillSource(source) {
|
|
90
|
+
if (source.kind === 'installed')
|
|
91
|
+
return `the installed skill at ${source.dir}/`;
|
|
92
|
+
return `the packaged skill text, inlined because ${source.reason}`;
|
|
93
|
+
}
|
|
94
|
+
function buildPrompt(root, skeletonDir, eligible, curated, source) {
|
|
95
|
+
const template = fs.readFileSync(path.join(skeletonDir, 'templates', 'bootstrap-docs-prompt.md'), 'utf-8');
|
|
96
|
+
let skillSource;
|
|
97
|
+
let skillText = '';
|
|
98
|
+
if (source.kind === 'installed') {
|
|
99
|
+
skillSource = `The skill is installed in this project at \`${source.dir}/\`: load it before you start, since it holds the rules for this work.`;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const skill = fs.readFileSync(path.join(skeletonDir, skillRelDir(source.provider ?? 'claude', BOOTSTRAP_SKILL), 'SKILL.md'), 'utf-8');
|
|
103
|
+
skillSource = `Its full text is at the end of this prompt, because ${source.reason}; follow that text, not any installed copy.`;
|
|
104
|
+
skillText = `\n## The ${BOOTSTRAP_SKILL} skill\n\n${skill.trim()}\n`;
|
|
105
|
+
}
|
|
106
|
+
return renderPrompt(template, {
|
|
107
|
+
PROJECT_NAME: path.basename(root),
|
|
108
|
+
SKILL_SOURCE: skillSource,
|
|
109
|
+
ELIGIBLE_FILES: fileList(eligible),
|
|
110
|
+
CURATED_FILES: fileList(curated),
|
|
111
|
+
SKILL_TEXT: skillText,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// =============================================================================
|
|
115
|
+
// Default environment
|
|
116
|
+
// =============================================================================
|
|
117
|
+
function resolveConfiguredLauncher(root) {
|
|
118
|
+
let config;
|
|
119
|
+
try {
|
|
120
|
+
config = loadConfig(root);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
return { supported: false, reason: `.codev/config.json could not be loaded: ${err.message}` };
|
|
124
|
+
}
|
|
125
|
+
return resolveInteractiveTaskLauncher(config.shell?.architect ?? 'claude', config.shell?.architectHarness, config.harness);
|
|
126
|
+
}
|
|
127
|
+
function isExecutableOnPath(executable) {
|
|
128
|
+
const canRun = (candidate) => {
|
|
129
|
+
try {
|
|
130
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
131
|
+
return fs.statSync(candidate).isFile();
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
if (executable.includes('/') || executable.includes(path.sep))
|
|
138
|
+
return canRun(executable);
|
|
139
|
+
const extensions = process.platform === 'win32' ? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT').split(';') : [''];
|
|
140
|
+
for (const dir of (process.env.PATH ?? '').split(path.delimiter)) {
|
|
141
|
+
if (dir === '')
|
|
142
|
+
continue;
|
|
143
|
+
for (const ext of extensions) {
|
|
144
|
+
if (canRun(path.join(dir, executable + ext)))
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Run one `consult` process. Its progress output is discarded (three in parallel would
|
|
152
|
+
* interleave on the terminal); the last stderr line is kept as the reason when it fails.
|
|
153
|
+
*/
|
|
154
|
+
function runConsultProcess(args, cwd) {
|
|
155
|
+
return new Promise(resolve => {
|
|
156
|
+
const child = spawn('consult', args, { cwd, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
157
|
+
let stderr = '';
|
|
158
|
+
child.stderr?.on('data', chunk => {
|
|
159
|
+
stderr = (stderr + String(chunk)).slice(-4000);
|
|
160
|
+
});
|
|
161
|
+
child.on('error', err => resolve({ code: null, signal: null, error: err.message }));
|
|
162
|
+
child.on('exit', (code, signal) => {
|
|
163
|
+
const lastLine = stderr.trim().split('\n').pop();
|
|
164
|
+
if (code !== 0 && lastLine) {
|
|
165
|
+
resolve({ code, signal, error: lastLine });
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
resolve({ code, signal });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/** Defaults for every seam but `skeletonDir`, which is resolved only when not overridden. */
|
|
174
|
+
function defaultDeps() {
|
|
175
|
+
return {
|
|
176
|
+
runConsult: runConsultProcess,
|
|
177
|
+
resolveLauncher: resolveConfiguredLauncher,
|
|
178
|
+
isExecutableAvailable: isExecutableOnPath,
|
|
179
|
+
confirm: question => confirm(question, false),
|
|
180
|
+
stdinIsTTY: Boolean(process.stdin.isTTY),
|
|
181
|
+
spawnAgent: spawnAgentProcess,
|
|
182
|
+
log: line => console.log(line),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function takeSnapshot(root, mode) {
|
|
186
|
+
if (mode !== 'filesystem') {
|
|
187
|
+
const snapshot = snapshotTree(root);
|
|
188
|
+
if (snapshot.available || mode === 'git') {
|
|
189
|
+
return { mode: 'git', snapshot, index: readIndexFingerprint(root) };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return { mode: 'filesystem', snapshot: snapshotFilesystem(root), index: null };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Each governance file's identity: the path itself, plus what it resolves to when it is a
|
|
196
|
+
* symlink, so a retarget and a write through the link are both a change.
|
|
197
|
+
*/
|
|
198
|
+
function hashGovernanceFiles(root) {
|
|
199
|
+
return new Map(GOVERNANCE_FILES.map(file => {
|
|
200
|
+
const absolute = path.join(root, governancePath(file.name));
|
|
201
|
+
let identity = hashPath(absolute);
|
|
202
|
+
if (identity.startsWith('<link:'))
|
|
203
|
+
identity += hashResolved(absolute);
|
|
204
|
+
return [file.name, identity];
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
function hashResolved(absolute) {
|
|
208
|
+
try {
|
|
209
|
+
return hashPath(fs.realpathSync(absolute));
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return '<dangling>';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function detectorScope(before, after) {
|
|
216
|
+
if (before.mode === 'git') {
|
|
217
|
+
return 'Change detection covered the git state and every tracked or untracked path git does not ignore; gitignored paths are not watched.';
|
|
218
|
+
}
|
|
219
|
+
if (before.snapshot.truncated || after.snapshot.truncated) {
|
|
220
|
+
return `Not a git repository, and the tree holds more than ${FILESYSTEM_WALK_LIMIT.toLocaleString('en-US')} files, so change detection covered only the four governance files.`;
|
|
221
|
+
}
|
|
222
|
+
const skipped = FILESYSTEM_WALK_SKIPPED.map(dir => `${dir}/`).join(', ');
|
|
223
|
+
return `Not a git repository: change detection walked every file except ${skipped} and paths the root .gitignore ignores (a repository created during the session is not seen).`;
|
|
224
|
+
}
|
|
225
|
+
function buildChangeReport(eligible, before, after, hashesBefore, hashesAfter) {
|
|
226
|
+
const governanceChanged = GOVERNANCE_FILES
|
|
227
|
+
.filter(file => !eligible.has(file.name) && hashesBefore.get(file.name) !== hashesAfter.get(file.name))
|
|
228
|
+
.map(file => governancePath(file.name));
|
|
229
|
+
const governancePaths = new Set(GOVERNANCE_FILES.map(file => governancePath(file.name)));
|
|
230
|
+
const eligiblePaths = new Set([...eligible].map(governancePath));
|
|
231
|
+
const isExempt = (rel) => eligiblePaths.has(rel) ||
|
|
232
|
+
// Reported above, by content hash, which also sees clean tracked files.
|
|
233
|
+
governancePaths.has(rel) ||
|
|
234
|
+
CONSULT_OWNED_PREFIXES.some(prefix => rel.startsWith(prefix));
|
|
235
|
+
const report = {
|
|
236
|
+
governanceChanged,
|
|
237
|
+
identityChanges: [],
|
|
238
|
+
indexChanged: false,
|
|
239
|
+
outOfScope: [],
|
|
240
|
+
scope: detectorScope(before, after),
|
|
241
|
+
};
|
|
242
|
+
if (before.snapshot.available && !after.snapshot.available) {
|
|
243
|
+
report.detectionLost = after.snapshot.reason ?? 'unknown';
|
|
244
|
+
return report;
|
|
245
|
+
}
|
|
246
|
+
if (before.snapshot.truncated || after.snapshot.truncated)
|
|
247
|
+
return report;
|
|
248
|
+
report.outOfScope = diffSnapshotEntries(before.snapshot, after.snapshot, isExempt).map(rel => {
|
|
249
|
+
const annotation = HARNESS_SCRATCH_PATHS[rel];
|
|
250
|
+
if (annotation)
|
|
251
|
+
return { path: rel, annotation };
|
|
252
|
+
return { path: rel };
|
|
253
|
+
});
|
|
254
|
+
report.identityChanges = diffRepositoryIdentity(before.snapshot, after.snapshot);
|
|
255
|
+
report.indexChanged = before.index !== null && after.index !== null && before.index !== after.index;
|
|
256
|
+
return report;
|
|
257
|
+
}
|
|
258
|
+
function reportHasChanges(report) {
|
|
259
|
+
return (report.governanceChanged.length > 0 ||
|
|
260
|
+
report.identityChanges.length > 0 ||
|
|
261
|
+
report.indexChanged ||
|
|
262
|
+
report.outOfScope.length > 0 ||
|
|
263
|
+
report.detectionLost !== undefined);
|
|
264
|
+
}
|
|
265
|
+
// =============================================================================
|
|
266
|
+
// This command's own files
|
|
267
|
+
// =============================================================================
|
|
268
|
+
/**
|
|
269
|
+
* Why `relPath` (one of this command's files under `.consult/bootstrap-docs/`) cannot be
|
|
270
|
+
* written safely, else undefined. A symlinked `.consult/`, directory or file would carry the
|
|
271
|
+
* write outside the project, before change detection has started.
|
|
272
|
+
*/
|
|
273
|
+
function artifactPathProblem(root, relPath) {
|
|
274
|
+
const ancestor = irregularAncestor(root, relPath);
|
|
275
|
+
if (ancestor !== undefined)
|
|
276
|
+
return `${relPath} would be written through ${ancestor}`;
|
|
277
|
+
let stat;
|
|
278
|
+
try {
|
|
279
|
+
stat = fs.lstatSync(path.join(root, relPath));
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
if (stat.isSymbolicLink())
|
|
285
|
+
return `${relPath} is a symlink`;
|
|
286
|
+
if (!stat.isFile())
|
|
287
|
+
return `${relPath} is not a regular file`;
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
/** Write one of this command's files, or return why it was not written. */
|
|
291
|
+
function writeArtifact(root, relPath, content) {
|
|
292
|
+
const problem = artifactPathProblem(root, relPath);
|
|
293
|
+
if (problem !== undefined)
|
|
294
|
+
return problem;
|
|
295
|
+
const full = path.join(root, relPath);
|
|
296
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
297
|
+
fs.rmSync(full, { force: true });
|
|
298
|
+
fs.writeFileSync(full, content, { flag: 'wx' });
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
// =============================================================================
|
|
302
|
+
// Validation
|
|
303
|
+
// =============================================================================
|
|
304
|
+
function readIfExists(filePath) {
|
|
305
|
+
if (!fs.existsSync(filePath))
|
|
306
|
+
return null;
|
|
307
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
308
|
+
}
|
|
309
|
+
function validateDrafts(root, eligible, templatesDir) {
|
|
310
|
+
return eligible.map(file => {
|
|
311
|
+
// A draft the session replaced with a symlink is never read through.
|
|
312
|
+
const kind = governancePathKind(root, file);
|
|
313
|
+
if (kind !== undefined) {
|
|
314
|
+
return { name: file.name, state: 'written', violations: [{ kind: 'not-regular', message: `the draft is now ${kind}, so it was not read` }], unmappedInfo: [] };
|
|
315
|
+
}
|
|
316
|
+
const content = readIfExists(path.join(root, governancePath(file.name)));
|
|
317
|
+
if (content === null)
|
|
318
|
+
return { name: file.name, state: 'deleted', violations: [], unmappedInfo: [] };
|
|
319
|
+
if (classifyProjectFile(root, file, templatesDir) === 'placeholder') {
|
|
320
|
+
return { name: file.name, state: 'unwritten', violations: [], unmappedInfo: [] };
|
|
321
|
+
}
|
|
322
|
+
if (file.kind === 'cold') {
|
|
323
|
+
return { name: file.name, state: 'written', violations: validateColdFile(content, true), unmappedInfo: [] };
|
|
324
|
+
}
|
|
325
|
+
const cold = readIfExists(path.join(root, governancePath(file.coldDoc))) ?? '';
|
|
326
|
+
const starter = knownStarterTexts(file, templatesDir)[0];
|
|
327
|
+
const { violations, unmappedSections } = validateHotFile(content, cold, file, starter);
|
|
328
|
+
let unmappedInfo = [];
|
|
329
|
+
if (topLevelHeadings(cold).length > HOT_CAP_MAP_TOPICS)
|
|
330
|
+
unmappedInfo = unmappedSections;
|
|
331
|
+
return { name: file.name, state: 'written', violations, unmappedInfo };
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
// =============================================================================
|
|
335
|
+
// Draft review
|
|
336
|
+
// =============================================================================
|
|
337
|
+
/** The argv for one reviewer: general mode, so no project detection is needed. */
|
|
338
|
+
export function reviewConsultArgs(model) {
|
|
339
|
+
return ['-m', model, '--prompt-file', REVIEW_PROMPT_FILE, '--output', `${REVIEW_DIR}/${model}.md`];
|
|
340
|
+
}
|
|
341
|
+
/** A code fence longer than any backtick run in `content`, so the draft cannot close it. */
|
|
342
|
+
function fenceFor(content) {
|
|
343
|
+
const longest = Math.max(0, ...(content.match(/`+/g) ?? []).map(run => run.length));
|
|
344
|
+
return '`'.repeat(Math.max(3, longest + 1));
|
|
345
|
+
}
|
|
346
|
+
function renderDrafts(root, names) {
|
|
347
|
+
return names
|
|
348
|
+
.map(name => {
|
|
349
|
+
const rel = governancePath(name);
|
|
350
|
+
const content = fs.readFileSync(path.join(root, rel), 'utf-8');
|
|
351
|
+
const fence = fenceFor(content);
|
|
352
|
+
return `### \`${rel}\`\n\n${fence}markdown\n${content.replace(/\n$/, '')}\n${fence}`;
|
|
353
|
+
})
|
|
354
|
+
.join('\n\n');
|
|
355
|
+
}
|
|
356
|
+
/** A consult artifact that records a non-blocking skip, e.g. Gemini without credentials. */
|
|
357
|
+
const SKIP_SUMMARY = /^SUMMARY: .*lane skipped\W+(.*)$/m;
|
|
358
|
+
/**
|
|
359
|
+
* Run the 3-way general consult on the written drafts and save the feedback under
|
|
360
|
+
* `.consult/bootstrap-docs/`. Never applies anything and never fails the pass: a reviewer
|
|
361
|
+
* that fails or skips is named, and `consult` being unavailable is a warning.
|
|
362
|
+
*/
|
|
363
|
+
async function reviewDrafts(root, skeletonDir, written, deps) {
|
|
364
|
+
if (written.length === 0)
|
|
365
|
+
return { ran: false, reviewers: [] };
|
|
366
|
+
const template = fs.readFileSync(path.join(skeletonDir, 'templates', 'bootstrap-docs-review-prompt.md'), 'utf-8');
|
|
367
|
+
const prompt = renderPrompt(template, {
|
|
368
|
+
PROJECT_NAME: path.basename(root),
|
|
369
|
+
DRAFTS: renderDrafts(root, written),
|
|
370
|
+
});
|
|
371
|
+
// consult writes each reviewer's feedback itself, so its paths are checked here too.
|
|
372
|
+
const reviewPaths = [REVIEW_PROMPT_FILE, ...REVIEW_MODELS.map(model => `${REVIEW_DIR}/${model}.md`)];
|
|
373
|
+
const blocked = reviewPaths.map(rel => artifactPathProblem(root, rel)).find(problem => problem !== undefined);
|
|
374
|
+
if (blocked !== undefined)
|
|
375
|
+
return { ran: true, unavailable: blocked, reviewers: [] };
|
|
376
|
+
writeArtifact(root, REVIEW_PROMPT_FILE, prompt);
|
|
377
|
+
deps.log('');
|
|
378
|
+
deps.log(`Reviewing the drafts with ${REVIEW_MODELS.join(', ')} (feedback goes to ${REVIEW_DIR}/; nothing is applied)…`);
|
|
379
|
+
const exits = await Promise.all(REVIEW_MODELS.map(model => {
|
|
380
|
+
const output = path.join(root, REVIEW_DIR, `${model}.md`);
|
|
381
|
+
// A file left by an earlier run must not pass for this run's feedback.
|
|
382
|
+
fs.rmSync(output, { force: true });
|
|
383
|
+
return deps.runConsult(reviewConsultArgs(model), root);
|
|
384
|
+
}));
|
|
385
|
+
const reviewers = REVIEW_MODELS.map((model, i) => {
|
|
386
|
+
const exit = exits[i];
|
|
387
|
+
const output = readIfExists(path.join(root, REVIEW_DIR, `${model}.md`));
|
|
388
|
+
if (exit.error !== undefined && exit.code === null)
|
|
389
|
+
return { model, status: 'failed', detail: exit.error };
|
|
390
|
+
if (exit.signal)
|
|
391
|
+
return { model, status: 'failed', detail: `terminated by ${exit.signal}` };
|
|
392
|
+
if (exit.code !== 0) {
|
|
393
|
+
let detail = `exit ${exit.code}`;
|
|
394
|
+
if (exit.error)
|
|
395
|
+
detail += `: ${exit.error}`;
|
|
396
|
+
return { model, status: 'failed', detail };
|
|
397
|
+
}
|
|
398
|
+
if (output === null || output.trim() === '')
|
|
399
|
+
return { model, status: 'failed', detail: 'no feedback was written' };
|
|
400
|
+
const skipped = output.match(SKIP_SUMMARY);
|
|
401
|
+
if (skipped)
|
|
402
|
+
return { model, status: 'skipped', detail: skipped[1].trim() };
|
|
403
|
+
return { model, status: 'ok' };
|
|
404
|
+
});
|
|
405
|
+
// Every reviewer failed to start: consult itself is missing or broken.
|
|
406
|
+
if (exits.every(exit => exit.code === null && exit.error !== undefined)) {
|
|
407
|
+
return { ran: true, unavailable: `\`consult\` could not run: ${exits[0].error}`, reviewers };
|
|
408
|
+
}
|
|
409
|
+
return { ran: true, reviewers };
|
|
410
|
+
}
|
|
411
|
+
function printReview(log, review) {
|
|
412
|
+
if (!review.ran)
|
|
413
|
+
return;
|
|
414
|
+
if (review.unavailable !== undefined) {
|
|
415
|
+
log(chalk.yellow(` Draft review did not run (${escapeControlChars(review.unavailable)}); the drafts were not reviewed.`));
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const reviewed = review.reviewers.filter(reviewer => reviewer.status === 'ok').map(reviewer => reviewer.model);
|
|
419
|
+
if (reviewed.length > 0) {
|
|
420
|
+
log(` Draft review: feedback from ${reviewed.join(', ')} is in ${REVIEW_DIR}/ (read it before committing; nothing was applied).`);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
log(chalk.yellow(' Draft review: no reviewer produced feedback.'));
|
|
424
|
+
}
|
|
425
|
+
for (const reviewer of review.reviewers) {
|
|
426
|
+
if (reviewer.status === 'ok')
|
|
427
|
+
continue;
|
|
428
|
+
log(chalk.yellow(` - ${reviewer.model} ${reviewer.status}: ${escapeControlChars(reviewer.detail ?? 'unknown')}`));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// =============================================================================
|
|
432
|
+
// Output
|
|
433
|
+
// =============================================================================
|
|
434
|
+
const CLASS_LABEL = {
|
|
435
|
+
placeholder: 'placeholder (eligible)',
|
|
436
|
+
curated: 'curated (read as input, not edited)',
|
|
437
|
+
missing: 'missing (run `codev update` to restore the starter, then re-run)',
|
|
438
|
+
};
|
|
439
|
+
function printFileStatuses(log, files) {
|
|
440
|
+
for (const status of files) {
|
|
441
|
+
let label = CLASS_LABEL[status.class];
|
|
442
|
+
if (status.heldBack)
|
|
443
|
+
label = `placeholder, held back: ${status.heldBack}`;
|
|
444
|
+
if (status.pathKind)
|
|
445
|
+
label = `${status.pathKind}, never rewritten (read as input, not edited)`;
|
|
446
|
+
log(` ${governancePath(status.file.name)}: ${label}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function quoteArg(arg) {
|
|
450
|
+
if (/^[\w@%+=:,./-]+$/.test(arg))
|
|
451
|
+
return arg;
|
|
452
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
453
|
+
}
|
|
454
|
+
/** The launch as a command line, with the (long) prompt argument shown by reference. */
|
|
455
|
+
function describeLaunch(launch, prompt) {
|
|
456
|
+
const args = launch.args.map(arg => {
|
|
457
|
+
if (arg === prompt)
|
|
458
|
+
return `"$(cat ${PROMPT_FILE})"`;
|
|
459
|
+
return quoteArg(arg);
|
|
460
|
+
});
|
|
461
|
+
return [quoteArg(launch.executable), ...args].join(' ');
|
|
462
|
+
}
|
|
463
|
+
/** Flags (or `--flag=value` forms) that let the agent act without asking. */
|
|
464
|
+
const BYPASS_FLAGS = [
|
|
465
|
+
'--dangerously-skip-permissions',
|
|
466
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
467
|
+
'--yolo',
|
|
468
|
+
'--permission-mode=bypassPermissions',
|
|
469
|
+
];
|
|
470
|
+
const PERMISSION_FLAG = /dangerously|bypass|yolo|full-auto|permission|approval|sandbox/i;
|
|
471
|
+
function describePermissions(launch, prompt) {
|
|
472
|
+
const flags = [];
|
|
473
|
+
const args = launch.args.filter(arg => arg !== prompt);
|
|
474
|
+
for (let i = 0; i < args.length; i++) {
|
|
475
|
+
if (!PERMISSION_FLAG.test(args[i]))
|
|
476
|
+
continue;
|
|
477
|
+
// Keep a separate value with its flag (`--permission-mode plan`).
|
|
478
|
+
if (!args[i].includes('=') && args[i + 1] !== undefined && !args[i + 1].startsWith('-')) {
|
|
479
|
+
flags.push(`${args[i]}=${args[i + 1]}`);
|
|
480
|
+
i++;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
flags.push(args[i]);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (flags.length === 0)
|
|
487
|
+
return 'Permission mode: the agent CLI\'s default (it asks before running commands or editing files).';
|
|
488
|
+
if (flags.some(flag => BYPASS_FLAGS.includes(flag))) {
|
|
489
|
+
return `Permission mode from your config: ${flags.join(' ')} (the agent will NOT ask before running commands or editing files).`;
|
|
490
|
+
}
|
|
491
|
+
return `Permission flags from your config: ${flags.join(' ')} (see your agent's documentation for what they allow).`;
|
|
492
|
+
}
|
|
493
|
+
function printSummary(log, validation, report, agentExit) {
|
|
494
|
+
log('');
|
|
495
|
+
log(chalk.bold('Bootstrap summary'));
|
|
496
|
+
if (agentExit.error) {
|
|
497
|
+
log(chalk.red(` Agent could not be started: ${agentExit.error}`));
|
|
498
|
+
log(` The prompt is saved at ${PROMPT_FILE} if you would rather run it in your own agent.`);
|
|
499
|
+
}
|
|
500
|
+
else if (agentExit.signal) {
|
|
501
|
+
log(chalk.red(` Agent was terminated by ${agentExit.signal}.`));
|
|
502
|
+
}
|
|
503
|
+
else if (agentExit.code !== 0) {
|
|
504
|
+
log(chalk.red(` Agent exited with status ${agentExit.code}.`));
|
|
505
|
+
}
|
|
506
|
+
log(' Drafts:');
|
|
507
|
+
for (const draft of validation) {
|
|
508
|
+
const rel = governancePath(draft.name);
|
|
509
|
+
if (draft.state === 'deleted') {
|
|
510
|
+
log(chalk.red(` ✗ ${rel}: deleted during the session (not written)`));
|
|
511
|
+
}
|
|
512
|
+
else if (draft.state === 'unwritten') {
|
|
513
|
+
log(chalk.red(` ✗ ${rel}: still the starter placeholder (not written)`));
|
|
514
|
+
}
|
|
515
|
+
else if (draft.violations.length === 0) {
|
|
516
|
+
log(chalk.green(` ✓ ${rel}: written, validation passed`));
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
log(chalk.red(` ✗ ${rel}: written, ${draft.violations.length} validation problem(s):`));
|
|
520
|
+
for (const violation of draft.violations)
|
|
521
|
+
log(` - ${escapeControlChars(violation.message)}`);
|
|
522
|
+
}
|
|
523
|
+
if (draft.unmappedInfo.length > 0) {
|
|
524
|
+
log(` info: cold sections not in the map (allowed above ${HOT_CAP_MAP_TOPICS}): ${draft.unmappedInfo.map(escapeControlChars).join(', ')}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (reportHasChanges(report)) {
|
|
528
|
+
log(chalk.red(' Changes outside the eligible files (nothing has been reverted):'));
|
|
529
|
+
printChanges(log, report, 'governance file that was not eligible');
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
log(' No changes outside the eligible files.');
|
|
533
|
+
}
|
|
534
|
+
if (report.detectionLost === undefined)
|
|
535
|
+
log(` ${report.scope}`);
|
|
536
|
+
}
|
|
537
|
+
/** One line per change in `report`; `governanceLabel` says why a governance file is listed. */
|
|
538
|
+
function printChanges(log, report, governanceLabel) {
|
|
539
|
+
for (const rel of report.governanceChanged)
|
|
540
|
+
log(chalk.red(` - ${rel} (${governanceLabel})`));
|
|
541
|
+
for (const change of report.identityChanges) {
|
|
542
|
+
log(chalk.red(` - ${change.field} moved: ${escapeControlChars(change.before)} -> ${escapeControlChars(change.after)} (check \`git reflog\`)`));
|
|
543
|
+
}
|
|
544
|
+
if (report.indexChanged)
|
|
545
|
+
log(chalk.red(' - the git index changed (check `git diff --cached`)'));
|
|
546
|
+
for (const change of report.outOfScope) {
|
|
547
|
+
let line = ` - ${escapeControlChars(change.path)}`;
|
|
548
|
+
if (change.annotation)
|
|
549
|
+
line += ` (${change.annotation})`;
|
|
550
|
+
log(chalk.red(line));
|
|
551
|
+
}
|
|
552
|
+
if (report.detectionLost !== undefined) {
|
|
553
|
+
log(chalk.red(` - the working tree could not be read afterwards (${escapeControlChars(report.detectionLost)}); whether anything else changed is unknown`));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function printFooter(log, report, review) {
|
|
557
|
+
printReview(log, review);
|
|
558
|
+
const duringReview = review.changesDuringReview;
|
|
559
|
+
if (duringReview !== undefined && reportHasChanges(duringReview)) {
|
|
560
|
+
log(chalk.red(' Changes made while the reviewers ran (nothing has been reverted):'));
|
|
561
|
+
printChanges(log, duringReview, 'changed during the review; the validation above describes the version before it');
|
|
562
|
+
}
|
|
563
|
+
const gitMoved = [report, duringReview].some(changes => changes !== undefined && (changes.identityChanges.length > 0 || changes.indexChanged));
|
|
564
|
+
log('');
|
|
565
|
+
if (gitMoved) {
|
|
566
|
+
log(chalk.yellow('Review before commit — Codev staged and committed nothing, but git state moved during the session (see above).'));
|
|
567
|
+
log(' Check `git log` and `git diff --cached` before trusting what is staged or committed.');
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
log(chalk.yellow('Review before commit — nothing has been staged or committed.'));
|
|
571
|
+
}
|
|
572
|
+
log(` Inspect the drafts with \`git diff\` (or open them), edit as needed, then commit them yourself.`);
|
|
573
|
+
}
|
|
574
|
+
// =============================================================================
|
|
575
|
+
// Command
|
|
576
|
+
// =============================================================================
|
|
577
|
+
export async function runBootstrapDocs(options = {}, overrides = {}) {
|
|
578
|
+
const deps = {
|
|
579
|
+
...defaultDeps(),
|
|
580
|
+
...overrides,
|
|
581
|
+
skeletonDir: overrides.skeletonDir ?? getTemplatesDir(),
|
|
582
|
+
};
|
|
583
|
+
const { log } = deps;
|
|
584
|
+
const root = path.resolve(options.cwd ?? process.cwd());
|
|
585
|
+
const templatesDir = path.join(deps.skeletonDir, 'templates');
|
|
586
|
+
if (!fs.existsSync(path.join(root, 'codev'))) {
|
|
587
|
+
log(chalk.red(`No codev/ directory in ${root}. Run \`codev adopt\` first, then re-run \`codev bootstrap-docs\` here.`));
|
|
588
|
+
return { exitCode: 1, outcome: 'no-codev', files: [], launched: false };
|
|
589
|
+
}
|
|
590
|
+
const files = classifyProjectGovernance(root, templatesDir);
|
|
591
|
+
const eligible = bootstrapEligibleFiles(files);
|
|
592
|
+
const curated = files.filter(status => status.class === 'curated').map(status => status.file.name);
|
|
593
|
+
log(chalk.bold('Governance docs:'));
|
|
594
|
+
printFileStatuses(log, files);
|
|
595
|
+
if (eligible.length === 0) {
|
|
596
|
+
log('');
|
|
597
|
+
log('Nothing needs bootstrapping: no governance file is still an untouched starter.');
|
|
598
|
+
if (files.some(status => status.class === 'missing')) {
|
|
599
|
+
log('Missing files are not created here: run `codev update` to restore their starters, then re-run.');
|
|
600
|
+
}
|
|
601
|
+
return { exitCode: 0, outcome: 'nothing-eligible', files, launched: false };
|
|
602
|
+
}
|
|
603
|
+
if (!isNonEmptyRepo(root)) {
|
|
604
|
+
log('');
|
|
605
|
+
log('This project holds nothing beyond the files Codev generated, so there is nothing to document yet.');
|
|
606
|
+
log('The placeholders stay; they fill in through review phases as work happens.');
|
|
607
|
+
return { exitCode: 0, outcome: 'empty-repo', files, launched: false };
|
|
608
|
+
}
|
|
609
|
+
const launcher = deps.resolveLauncher(root);
|
|
610
|
+
let unavailable;
|
|
611
|
+
if (!launcher.supported) {
|
|
612
|
+
unavailable = launcher.reason;
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
const executable = launcher.buildLaunch('').executable;
|
|
616
|
+
if (!deps.isExecutableAvailable(executable))
|
|
617
|
+
unavailable = `"${executable}" was not found on PATH`;
|
|
618
|
+
}
|
|
619
|
+
// The installed skill is named only for a session this command launches itself; a prompt
|
|
620
|
+
// saved for a manual run may go to an agent that does not load project skills.
|
|
621
|
+
let launchingHarness;
|
|
622
|
+
if (launcher.supported && !unavailable)
|
|
623
|
+
launchingHarness = launcher.harnessName;
|
|
624
|
+
const skillSource = chooseSkillSource(root, deps.skeletonDir, launchingHarness);
|
|
625
|
+
if (options.dryRun) {
|
|
626
|
+
log('');
|
|
627
|
+
if (launcher.supported && !unavailable) {
|
|
628
|
+
const launch = launcher.buildLaunch('<bootstrap prompt>');
|
|
629
|
+
log(`Would launch (${launcher.harnessName}): ${[launch.executable, ...launch.args.map(quoteArg)].join(' ')}`);
|
|
630
|
+
log(`The agent would follow ${describeSkillSource(skillSource)}.`);
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
log(`Would not launch an agent: ${unavailable}.`);
|
|
634
|
+
log(`Would write the prompt to ${PROMPT_FILE} for you to run in your own agent.`);
|
|
635
|
+
}
|
|
636
|
+
log('Dry run: nothing launched, nothing written.');
|
|
637
|
+
return { exitCode: 0, outcome: 'dry-run', files, launched: false };
|
|
638
|
+
}
|
|
639
|
+
const prompt = buildPrompt(root, deps.skeletonDir, eligible.map(file => file.name), curated, skillSource);
|
|
640
|
+
const promptProblem = writeArtifact(root, PROMPT_FILE, prompt);
|
|
641
|
+
if (promptProblem !== undefined) {
|
|
642
|
+
log(chalk.red(`Cannot save the bootstrap prompt: ${escapeControlChars(promptProblem)}.`));
|
|
643
|
+
log('Nothing was launched. Make it a regular path inside the project, then run `codev bootstrap-docs` again.');
|
|
644
|
+
return { exitCode: 1, outcome: 'unsafe-path', files, launched: false };
|
|
645
|
+
}
|
|
646
|
+
const promptBytes = Buffer.byteLength(prompt);
|
|
647
|
+
if (!unavailable && promptBytes > MAX_PROMPT_BYTES) {
|
|
648
|
+
unavailable = `the prompt is ${Math.ceil(promptBytes / 1000)} KB, too long to pass as a command-line argument (limit ${MAX_PROMPT_BYTES / 1000} KB)`;
|
|
649
|
+
}
|
|
650
|
+
if (!launcher.supported || unavailable) {
|
|
651
|
+
log('');
|
|
652
|
+
log(chalk.yellow(`Not launching an agent: ${unavailable}.`));
|
|
653
|
+
log(`The bootstrap prompt is saved at ${PROMPT_FILE}. To draft the docs yourself:`);
|
|
654
|
+
log(' 1. Open your agent in this directory.');
|
|
655
|
+
log(` 2. Give it the contents of ${PROMPT_FILE} as its first message.`);
|
|
656
|
+
log(' 3. Review the drafts before committing; the placeholders stay until it writes them.');
|
|
657
|
+
return { exitCode: 0, outcome: 'manual', files, launched: false };
|
|
658
|
+
}
|
|
659
|
+
const launch = launcher.buildLaunch(prompt);
|
|
660
|
+
const command = describeLaunch(launch, prompt);
|
|
661
|
+
log('');
|
|
662
|
+
log(chalk.bold('About to launch an agent session to draft the governance docs:'));
|
|
663
|
+
log(` Command: ${command}`);
|
|
664
|
+
log(' It reads the codebase and explores the git history with read-only git commands.');
|
|
665
|
+
log(` It follows ${describeSkillSource(skillSource)}.`);
|
|
666
|
+
log(` It may write only: ${eligible.map(file => governancePath(file.name)).join(', ')}`);
|
|
667
|
+
log(' It consumes model quota on your agent account.');
|
|
668
|
+
log(` Afterwards the drafts are sent to ${REVIEW_MODELS.join(', ')} for review, through \`consult\`.`);
|
|
669
|
+
log(` ${describePermissions(launch, prompt)}`);
|
|
670
|
+
log(' When it says the drafts are done, exit the session; Codev then checks what changed.');
|
|
671
|
+
if (!deps.stdinIsTTY) {
|
|
672
|
+
log('');
|
|
673
|
+
log('Standard input is not a terminal, so the launch is declined. Run it interactively:');
|
|
674
|
+
log(' codev bootstrap-docs');
|
|
675
|
+
log(`The prompt is saved at ${PROMPT_FILE} if you would rather run it in your own agent.`);
|
|
676
|
+
return { exitCode: 0, outcome: 'declined', files, launched: false };
|
|
677
|
+
}
|
|
678
|
+
if (!(await deps.confirm('Launch the agent now?'))) {
|
|
679
|
+
log('Declined; nothing launched. The placeholders are unchanged.');
|
|
680
|
+
log(`The prompt is saved at ${PROMPT_FILE} if you would rather run it in your own agent.`);
|
|
681
|
+
return { exitCode: 0, outcome: 'declined', files, launched: false };
|
|
682
|
+
}
|
|
683
|
+
const eligibleNames = new Set(eligible.map(file => file.name));
|
|
684
|
+
const hashesBefore = hashGovernanceFiles(root);
|
|
685
|
+
const before = takeSnapshot(root);
|
|
686
|
+
const agentExit = await deps.spawnAgent(launch, root);
|
|
687
|
+
const after = takeSnapshot(root, before.mode);
|
|
688
|
+
const hashesAfter = hashGovernanceFiles(root);
|
|
689
|
+
const report = buildChangeReport(eligibleNames, before, after, hashesBefore, hashesAfter);
|
|
690
|
+
const validation = validateDrafts(root, eligible, templatesDir);
|
|
691
|
+
// A draft replaced by a symlink is never read, so it is never sent to the reviewers either.
|
|
692
|
+
const written = validation
|
|
693
|
+
.filter(draft => draft.state === 'written' && !draft.violations.some(violation => violation.kind === 'not-regular'))
|
|
694
|
+
.map(draft => draft.name);
|
|
695
|
+
// The summary prints before the review, which can take minutes, so an interrupted review
|
|
696
|
+
// never costs the human the change report.
|
|
697
|
+
printSummary(log, validation, report, agentExit);
|
|
698
|
+
let review;
|
|
699
|
+
try {
|
|
700
|
+
review = await reviewDrafts(root, deps.skeletonDir, written, deps);
|
|
701
|
+
}
|
|
702
|
+
catch (err) {
|
|
703
|
+
review = { ran: true, unavailable: `the review could not be prepared: ${err.message}`, reviewers: [] };
|
|
704
|
+
}
|
|
705
|
+
// The reviewers are agents too: anything they changed, drafts included, is named and fails
|
|
706
|
+
// the run, measured from the post-session state (nothing is eligible to change here).
|
|
707
|
+
const changesDuringReview = buildChangeReport(new Set(), after, takeSnapshot(root, before.mode), hashesAfter, hashGovernanceFiles(root));
|
|
708
|
+
review.changesDuringReview = changesDuringReview;
|
|
709
|
+
printFooter(log, report, review);
|
|
710
|
+
const agentFailed = agentExit.error !== undefined || agentExit.signal !== null || agentExit.code !== 0;
|
|
711
|
+
const draftsFailed = validation.some(draft => draft.state !== 'written' || draft.violations.length > 0);
|
|
712
|
+
let exitCode = 0;
|
|
713
|
+
if (agentFailed || draftsFailed || reportHasChanges(report))
|
|
714
|
+
exitCode = 1;
|
|
715
|
+
if (reportHasChanges(changesDuringReview))
|
|
716
|
+
exitCode = 1;
|
|
717
|
+
// Reviewer verdicts inform the human and never change the exit code; changes they made do.
|
|
718
|
+
return { exitCode, outcome: 'ran', files, launched: true, agentExit, report, validation, review };
|
|
719
|
+
}
|
|
720
|
+
/** CLI entry: run the pass and hand back the process exit code. */
|
|
721
|
+
export async function bootstrapDocs(options = {}) {
|
|
722
|
+
const result = await runBootstrapDocs(options);
|
|
723
|
+
return result.exitCode;
|
|
724
|
+
}
|
|
725
|
+
//# sourceMappingURL=bootstrap-docs.js.map
|