codictator 0.1.2 → 0.2.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.
Files changed (71) hide show
  1. package/dist/builtin-packs/core/fixtures/conventional-commit/passing/basic/case.json +1 -1
  2. package/dist/builtin-packs/core/fixtures/conventional-commit/violating/basic/case.json +1 -1
  3. package/dist/builtin-packs/typescript/fixtures/lint/passing/exported/case.json +8 -0
  4. package/dist/builtin-packs/typescript/pack.json +1 -1
  5. package/dist/cli/commands.d.ts +1 -1
  6. package/dist/cli/commands.js +3 -1
  7. package/dist/cli/gate-scope.d.ts +1 -0
  8. package/dist/cli/gate-scope.js +11 -0
  9. package/dist/cli/git-hook.js +13 -8
  10. package/dist/cli/guardrail.d.ts +2 -0
  11. package/dist/cli/guardrail.js +11 -13
  12. package/dist/cli/init-plan.d.ts +7 -0
  13. package/dist/cli/init-plan.js +21 -2
  14. package/dist/cli/init.d.ts +2 -0
  15. package/dist/cli/init.js +8 -17
  16. package/dist/cli/plan-launcher.d.ts +1 -0
  17. package/dist/cli/plan-launcher.js +53 -3
  18. package/dist/cli/serve-run.js +1 -0
  19. package/dist/cli/setup-gitignore.d.ts +7 -0
  20. package/dist/cli/setup-gitignore.js +41 -0
  21. package/dist/cli/setup-handoff.d.ts +7 -0
  22. package/dist/cli/setup-handoff.js +44 -0
  23. package/dist/cli/setup-install.d.ts +11 -0
  24. package/dist/cli/setup-install.js +84 -0
  25. package/dist/cli/setup.d.ts +9 -0
  26. package/dist/cli/setup.js +143 -0
  27. package/dist/cli/task-advance.js +16 -16
  28. package/dist/features/adapters/claude-shaped.d.ts +1 -0
  29. package/dist/features/adapters/claude-shaped.js +29 -31
  30. package/dist/features/adapters/copilot.js +22 -29
  31. package/dist/features/audit/append.js +9 -4
  32. package/dist/features/audit/surfaces.js +15 -8
  33. package/dist/features/ci/protection.js +37 -13
  34. package/dist/features/deps/npm.js +39 -24
  35. package/dist/features/deps/registry.js +45 -34
  36. package/dist/features/deps/stable.js +52 -27
  37. package/dist/features/deps/upgrade.d.ts +1 -1
  38. package/dist/features/deps/upgrade.js +29 -19
  39. package/dist/features/engine/evaluate.js +6 -1
  40. package/dist/features/packs/enforcers/ci-guards.js +4 -1
  41. package/dist/features/packs/enforcers/code.js +2 -1
  42. package/dist/features/packs/enforcers/engine-version-guards.d.ts +6 -0
  43. package/dist/features/packs/enforcers/engine-version-guards.js +42 -0
  44. package/dist/features/packs/enforcers/external-bind.js +62 -28
  45. package/dist/features/packs/enforcers/external-sarif.js +30 -24
  46. package/dist/features/packs/enforcers/gate-guards.d.ts +0 -1
  47. package/dist/features/packs/enforcers/gate-guards.js +83 -136
  48. package/dist/features/packs/enforcers/health-measures.d.ts +2 -0
  49. package/dist/features/packs/enforcers/health-measures.js +10 -1
  50. package/dist/features/packs/enforcers/health-refs.d.ts +1 -2
  51. package/dist/features/packs/enforcers/health-refs.js +71 -56
  52. package/dist/features/packs/enforcers/secret-scan.d.ts +4 -20
  53. package/dist/features/packs/enforcers/secret-scan.js +61 -67
  54. package/dist/features/packs/enforcers/ts-checks.js +104 -58
  55. package/dist/features/packs/enforcers/ts-security-patterns.js +12 -8
  56. package/dist/features/packs/manifest-decl.d.ts +13 -0
  57. package/dist/features/packs/manifest-decl.js +121 -0
  58. package/dist/features/packs/manifest.d.ts +2 -12
  59. package/dist/features/packs/manifest.js +5 -90
  60. package/dist/features/packs/selftest.js +22 -5
  61. package/dist/features/remediation/adoption.js +40 -29
  62. package/dist/features/testing/scenarios.js +49 -27
  63. package/dist/features/testing/tap-parser.js +38 -26
  64. package/dist/features/workflow/phases.js +45 -42
  65. package/dist/features/workflow/review.js +17 -10
  66. package/dist/kernel/git.js +4 -1
  67. package/dist/kernel/pattern.js +120 -86
  68. package/dist/kernel/secret-paths.js +34 -20
  69. package/dist/kernel/shell-cmd.js +23 -6
  70. package/dist/tsconfig.build.tsbuildinfo +1 -1
  71. package/package.json +1 -1
@@ -5,6 +5,6 @@
5
5
  "trigger": "commit subject with no type prefix",
6
6
  "expectRules": [],
7
7
  "files": {
8
- "commit.txt": "subject: fix(core): drop dead export\n"
8
+ "sample.txt": "subject: fix(core): drop dead export\n"
9
9
  }
10
10
  }
@@ -5,6 +5,6 @@
5
5
  "trigger": "commit subject with no type prefix",
6
6
  "expectRules": ["core/conventional-commit"],
7
7
  "files": {
8
- "commit.txt": "subject: fixed stuff\n"
8
+ "sample.txt": "subject: fixed stuff\n"
9
9
  }
10
10
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "expectation": "passing",
3
+ "expectRules": [],
4
+ "files": { "sample.ts": "export const MS_PER_DAY = 86_400_000;\n" },
5
+ "rule": "ts/lint",
6
+ "section": "§6.1",
7
+ "trigger": "exported binding used cross-file, not in its own file"
8
+ }
@@ -31,7 +31,7 @@
31
31
  "params": {},
32
32
  "ratchet": false,
33
33
  "measureVersion": 1,
34
- "fixtures": { "violating": ["basic"], "passing": ["basic"] }
34
+ "fixtures": { "violating": ["basic"], "passing": ["basic", "exported"] }
35
35
  },
36
36
  {
37
37
  "id": "ts/no-phantom-import",
@@ -2,4 +2,4 @@ import type { Result } from "../kernel/index.ts";
2
2
  import type { CommandRequest } from "./shared.ts";
3
3
  export type Handler = (req: CommandRequest) => Promise<Result<number>>;
4
4
  export declare const COMMANDS: Readonly<Record<string, Handler>>;
5
- export declare const USAGE = "usage: codictator <init|install-hooks|regen|uninstall|verify|status|explain|replay|report|context|hook|task|test|review|waive|baseline|deps|remediate|ci|generate|pack|adapter|serve> [--json] [--cwd <path>] [--quiet]";
5
+ export declare const USAGE = "usage: codictator <init|setup|install-hooks|regen|uninstall|verify|status|explain|replay|report|context|hook|task|test|review|waive|baseline|deps|remediate|ci|generate|pack|adapter|serve> [--json] [--cwd <path>] [--quiet]";
@@ -9,11 +9,13 @@ import { handleReview } from "./proofs.js";
9
9
  import { handleTestCmd } from "./proofs-test.js";
10
10
  import { handleRemediate } from "./remediation.js";
11
11
  import { handleContext, handleReport } from "./report.js";
12
+ import { handleSetup } from "./setup.js";
12
13
  import { handleTask } from "./task-advance.js";
13
14
  import { handleExplain, handleReplay } from "./transparency.js";
14
15
  import { handleWaive } from "./waivers.js";
15
16
  export const COMMANDS = {
16
17
  init: handleInit,
18
+ setup: handleSetup,
17
19
  "install-hooks": handleInstallHooks,
18
20
  regen: handleRegen,
19
21
  uninstall: handleUninstall,
@@ -36,4 +38,4 @@ export const COMMANDS = {
36
38
  adapter: handleAdapter,
37
39
  serve: handleServe,
38
40
  };
39
- export const USAGE = "usage: codictator <init|install-hooks|regen|uninstall|verify|status|explain|replay|report|context|hook|task|test|review|waive|baseline|deps|remediate|ci|generate|pack|adapter|serve> [--json] [--cwd <path>] [--quiet]";
41
+ export const USAGE = "usage: codictator <init|setup|install-hooks|regen|uninstall|verify|status|explain|replay|report|context|hook|task|test|review|waive|baseline|deps|remediate|ci|generate|pack|adapter|serve> [--json] [--cwd <path>] [--quiet]";
@@ -7,4 +7,5 @@ export interface GateScope {
7
7
  readonly note: string;
8
8
  readonly proposedContent?: string;
9
9
  }
10
+ export declare function addedLines(diff: string): string;
10
11
  export declare function evaluateGateScope(ports: CommandPorts, packIds: ReadonlyArray<string>, scope: GateScope, now: string, configHash: string): Promise<Result<Array<Finding>>>;
@@ -51,6 +51,17 @@ function mergeGateFindings(first, second) {
51
51
  }
52
52
  return merged.sort(compareGateFindings);
53
53
  }
54
+ // BUG-0022 + SPEC §4.7.1: the L3 gate judges added lines only, so a commit
55
+ // deleting a flagged literal passes while staging one still blocks. A diff
56
+ // carries its removed lines as `-` rows; scanning the raw diff would match
57
+ // those and make the removal uncommittable.
58
+ export function addedLines(diff) {
59
+ return diff
60
+ .split("\n")
61
+ .filter((line) => line.startsWith("+") && !line.startsWith("+++"))
62
+ .map((line) => line.slice(1))
63
+ .join("\n");
64
+ }
54
65
  // BUG-0014 + SPEC §4.3.2: the gate evaluation over a diff scope. The
55
66
  // pre-tool leg keeps today's fail-closed local verdict byte for byte; the
56
67
  // review leg adds the diff-sound review subset over the same files, so the
@@ -4,9 +4,10 @@
4
4
  // the change with the gate rule subset `verify` loads over staged scopes —
5
5
  // new scope plumbing, no new detection logic. commit-msg appends
6
6
  // the Codictator-L3 trailer §4.14.2 checks at L4.
7
+ import path from "node:path";
7
8
  import { ENGINE_VERSION, HOOK_MARKER } from "../features/config/index.js";
8
9
  import { err, makeError, ok, readHostText, writeHostText, } from "../kernel/index.js";
9
- import { evaluateGateScope } from "./gate-scope.js";
10
+ import { addedLines, evaluateGateScope } from "./gate-scope.js";
10
11
  import { printFinding } from "./inspect.js";
11
12
  import { commandEnv, fail, loadRepoConfig, MIN_NODE_MAJOR, needRoot, nodeMajor, nodeTooOldMessage, printHuman, printJson, reportError, usageError, } from "./shared.js";
12
13
  const GIT_TIMEOUT_MS = 30000;
@@ -53,7 +54,7 @@ async function evaluateGitChange(ports, req, hook, packIds, files, content, now,
53
54
  const evaluated = await evaluateGateScope(ports, packIds, {
54
55
  files: [...files],
55
56
  note: hook,
56
- ...(content === "" ? {} : { proposedContent: content }),
57
+ ...(content === "" ? {} : { proposedContent: addedLines(content) }),
57
58
  }, now, configHash);
58
59
  if (!evaluated.ok) {
59
60
  return reportError(evaluated.error, req.json);
@@ -77,16 +78,20 @@ async function runPreCommit(req, ports, packIds, now, configHash) {
77
78
  }
78
79
  return evaluateGitChange(ports, req, "pre-commit", packIds, staged.value, diff.value, now, configHash);
79
80
  }
80
- // SPEC §4.14.4: git hands commit-msg an absolute COMMIT_EDITMSG path, which
81
- // in a linked worktree lives under the main repo's .git/worktrees/<name>/
82
- // dir, outside the worktree root. The confined Fs port rejects absolute
83
- // paths (CDX-E701), so the hook uses kernel-owned host I/O; an unreadable
84
- // or unwritable message file blocks fail-closed.
81
+ // SPEC §4.14.4: git hands commit-msg the COMMIT_EDITMSG path, absolute in a
82
+ // linked worktree (under the main repo's .git/worktrees/<name>/ dir, outside
83
+ // the worktree root) but relative to the repo root (`.git/COMMIT_EDITMSG`)
84
+ // in the main worktree. The confined Fs port rejects absolute paths
85
+ // (CDX-E701), so the hook uses kernel-owned host I/O — which itself
86
+ // requires absolute paths — resolving a relative handoff against the
87
+ // invocation cwd first; an unreadable or unwritable message file blocks
88
+ // fail-closed.
85
89
  async function runCommitMsg(req, configHash) {
86
90
  if (req.args.length !== 2 || req.args[1] === undefined) {
87
91
  return usageError("usage: codictator git commit-msg <file>");
88
92
  }
89
- const file = req.args[1];
93
+ const raw = req.args[1];
94
+ const file = path.isAbsolute(raw) ? raw : path.resolve(req.cwd, raw);
90
95
  const trailer = `Codictator-L3: ${configHash.slice(0, 12)}`;
91
96
  const read = await readHostText(file);
92
97
  if (!read.ok) {
@@ -1,5 +1,7 @@
1
1
  import type { Result } from "../kernel/index.ts";
2
2
  import type { CommandRequest } from "./shared.ts";
3
3
  export declare function handleRegen(req: CommandRequest): Promise<Result<number>>;
4
+ export declare function writeHookFiles(root: string): Promise<Result<true>>;
5
+ export declare function configureHooksPath(root: string, req: CommandRequest): Promise<void>;
4
6
  export declare function handleInstallHooks(req: CommandRequest): Promise<Result<number>>;
5
7
  export declare function handleUninstall(req: CommandRequest): Promise<Result<number>>;
@@ -7,8 +7,9 @@ import { renderCustomRegenFragment } from "../features/adapters/index.js";
7
7
  import { ENGINE_VERSION } from "../features/config/index.js";
8
8
  import { EXPECTED_HOOKS_PATH, HOOK_NAMES, hookInstallPlan, renderHookScript, } from "../features/integrity/index.js";
9
9
  import { err, ok, resolveRepoRoot } from "../kernel/index.js";
10
+ import { initPreamble } from "./init-plan.js";
10
11
  import { guardrailPlan, writePlan } from "./plan.js";
11
- import { checkHuman, commandEnv, fail, loadRepoConfig, portsFor, printHuman, printJson, readStateText, usageError, } from "./shared.js";
12
+ import { fail, loadRepoConfig, portsFor, printHuman, printJson, readStateText, usageError, } from "./shared.js";
12
13
  function repoNameOf(root) {
13
14
  return root.replaceAll("\\", "/").split("/").pop() ?? "repo";
14
15
  }
@@ -89,7 +90,7 @@ export async function handleRegen(req) {
89
90
  function encode(text) {
90
91
  return new TextEncoder().encode(text);
91
92
  }
92
- async function writeHookFiles(root) {
93
+ export async function writeHookFiles(root) {
93
94
  const ports = await portsFor(root);
94
95
  if (!ports.ok) {
95
96
  return err(ports.error);
@@ -107,7 +108,7 @@ async function writeHookFiles(root) {
107
108
  }
108
109
  return ok(true);
109
110
  }
110
- async function configureHooksPath(root, req) {
111
+ export async function configureHooksPath(root, req) {
111
112
  const ports = await portsFor(root);
112
113
  if (!ports.ok) {
113
114
  return;
@@ -169,21 +170,18 @@ export async function handleUninstall(req) {
169
170
  if (rejected !== undefined) {
170
171
  return rejected;
171
172
  }
172
- const human = checkHuman(commandEnv(), []);
173
- if (!human.ok) {
174
- return fail("CDX-E107", human.error.message, human.error.remediation, req.json);
173
+ const preamble = await initPreamble(req);
174
+ if (typeof preamble !== "string") {
175
+ return preamble;
175
176
  }
176
- const root = await resolveRepoRoot(req.cwd);
177
- if (!root.ok) {
178
- return fail("CDX-E703", root.error.message, root.error.remediation, req.json);
179
- }
180
- await removeGuardrails(root.value);
177
+ const root = preamble;
178
+ await removeGuardrails(root);
181
179
  const note = "CI will fail until its workflow and required check are removed too.";
182
180
  if (req.json) {
183
- printJson({ uninstalled: true, root: root.value, note });
181
+ printJson({ uninstalled: true, root, note });
184
182
  }
185
183
  else {
186
- printHuman(`uninstalled guardrails from ${root.value}`, req.quiet);
184
+ printHuman(`uninstalled guardrails from ${root}`, req.quiet);
187
185
  printHuman(note, req.quiet);
188
186
  }
189
187
  return ok(0);
@@ -20,4 +20,11 @@ export declare function emitInitResult(req: CommandRequest, root: string, writte
20
20
  path: string;
21
21
  }>, baselineWritten: boolean): Result<number>;
22
22
  export declare function initPreamble(req: CommandRequest): Promise<string | Result<number>>;
23
+ export interface InitSequence {
24
+ readonly written: ReadonlyArray<{
25
+ path: string;
26
+ }>;
27
+ readonly baseline: boolean;
28
+ }
29
+ export declare function runInitSequence(root: string, req: CommandRequest, flags: InitFlags, planned: ReadonlyArray<string>, engineVersion: string): Promise<Result<InitSequence>>;
23
30
  export declare function buildGuardrailPlan(root: string, flags: InitFlags, planned: ReadonlyArray<string>, engineVersion?: string): Promise<ReturnType<typeof guardrailPlan>>;
@@ -4,8 +4,8 @@
4
4
  import { CODICTATOR_MODULES } from "../features/arch/index.js";
5
5
  import { ENGINE_VERSION } from "../features/config/index.js";
6
6
  import { captureBaseline } from "../features/remediation/index.js";
7
- import { ok, parseJson, resolveRepoRoot, stableStringify, } from "../kernel/index.js";
8
- import { buildHelperTool, guardrailPlan } from "./plan.js";
7
+ import { err, ok, parseJson, resolveRepoRoot, stableStringify, } from "../kernel/index.js";
8
+ import { buildHelperTool, guardrailPlan, writePlan } from "./plan.js";
9
9
  import { checkHuman, commandEnv, fail, loadRepoConfig, printHuman, printJson, readStateText, writeStateText, } from "./shared.js";
10
10
  // SPEC §4.19.2: a helper tool proposal names the helper role, the binary
11
11
  // and the exact pin from the human's registry lookup: role=command@pin.
@@ -114,6 +114,25 @@ export async function initPreamble(req) {
114
114
  }
115
115
  return root.value;
116
116
  }
117
+ // SPEC §4.13.1: the shared write-then-guard tail of the init path: plan,
118
+ // write, fresh-config guard, baseline guard. init and setup run the same
119
+ // tail; one helper owns it.
120
+ export async function runInitSequence(root, req, flags, planned, engineVersion) {
121
+ const plan = await buildGuardrailPlan(root, flags, planned, engineVersion);
122
+ const wrote = await writePlan(root, plan, engineVersion);
123
+ if (!wrote.ok) {
124
+ return fail("CDX-E903", wrote.error.message, wrote.error.remediation, req.json);
125
+ }
126
+ const fresh = await checkFreshConfig(root, req);
127
+ if (!fresh.ok) {
128
+ return err(fresh.error);
129
+ }
130
+ const baseline = await ensureBaseline(root, req);
131
+ if (!baseline.ok) {
132
+ return fail(baseline.error.code, baseline.error.message, baseline.error.remediation, req.json);
133
+ }
134
+ return ok({ written: wrote.value, baseline: baseline.value });
135
+ }
117
136
  // The plan assembly handleInit writes: read-only inputs in, GuardrailPlan
118
137
  // out. Writing stays on the command so the plan builder never touches
119
138
  // the disk.
@@ -1,3 +1,5 @@
1
1
  import type { Result } from "../kernel/index.ts";
2
+ import { type InitFlags } from "./init-plan.ts";
2
3
  import type { CommandRequest } from "./shared.ts";
4
+ export declare function parseInitFlags(args: ReadonlyArray<string>): InitFlags | undefined;
3
5
  export declare function handleInit(req: CommandRequest): Promise<Result<number>>;
package/dist/cli/init.js CHANGED
@@ -4,9 +4,9 @@
4
4
  import { parseArgs } from "node:util";
5
5
  import { ENGINE_VERSION } from "../features/config/index.js";
6
6
  import { isBuiltinPackId } from "../features/packs/index.js";
7
- import { buildGuardrailPlan, checkFreshConfig, defaultHarnesses, emitInitResult, ensureBaseline, initPreamble, reportExisting, } from "./init-plan.js";
8
- import { KNOWN_HELPER_ROLES, writePlan } from "./plan.js";
9
- import { fail, readStateText, usageError } from "./shared.js";
7
+ import { defaultHarnesses, emitInitResult, initPreamble, reportExisting, runInitSequence, } from "./init-plan.js";
8
+ import { KNOWN_HELPER_ROLES } from "./plan.js";
9
+ import { readStateText, usageError } from "./shared.js";
10
10
  const KNOWN_PROFILES = new Set([
11
11
  "app",
12
12
  "library",
@@ -55,7 +55,7 @@ function narrowInitTools(specs) {
55
55
  }
56
56
  return tools;
57
57
  }
58
- function parseInitFlags(args) {
58
+ export function parseInitFlags(args) {
59
59
  try {
60
60
  const parsed = parseArgs({
61
61
  args: [...args],
@@ -105,18 +105,9 @@ export async function handleInit(req) {
105
105
  if (existing !== undefined && !flags.repair) {
106
106
  return reportExisting(req, root);
107
107
  }
108
- const plan = await buildGuardrailPlan(root, flags, planned, engineVersion);
109
- const wrote = await writePlan(root, plan, engineVersion);
110
- if (!wrote.ok) {
111
- return fail("CDX-E903", wrote.error.message, wrote.error.remediation, req.json);
108
+ const sequenced = await runInitSequence(root, req, flags, planned, engineVersion);
109
+ if (!sequenced.ok) {
110
+ return sequenced;
112
111
  }
113
- const fresh = await checkFreshConfig(root, req);
114
- if (!fresh.ok) {
115
- return fresh;
116
- }
117
- const baseline = await ensureBaseline(root, req);
118
- if (!baseline.ok) {
119
- return fail(baseline.error.code, baseline.error.message, baseline.error.remediation, req.json);
120
- }
121
- return emitInitResult(req, root, wrote.value, baseline.value);
112
+ return emitInitResult(req, root, sequenced.value.written, sequenced.value.baseline);
122
113
  }
@@ -1 +1,2 @@
1
1
  export declare const LAUNCHER_TEXT: string;
2
+ export type LauncherText = typeof LAUNCHER_TEXT;
@@ -20,7 +20,11 @@ export const LAUNCHER_TEXT = [
20
20
  "// any newer syntax could load, and only node: built-ins are imported. It",
21
21
  "// sets CODICTATOR_HOOK=1 in the engine process, then resolves the engine at",
22
22
  "// <root>/node_modules/codictator/dist/cli/main.js else",
23
- "// <root>/.codictator/tool/node_modules/codictator/dist/cli/main.js. A",
23
+ "// <root>/.codictator/tool/node_modules/codictator/dist/cli/main.js. The",
24
+ "// launcher reads the resolved engine's manifest version and passes it",
25
+ "// as main's third argument, so hook validation sees the installed",
26
+ "// version (SPEC §4.2.1 EC-11); an unreadable manifest falls back to",
27
+ "// the two-argument call. A",
24
28
  "// missing engine answers the harness's embedded static CDX-E003 block",
25
29
  "// reply; outside hook it prints CDX-E003 and exits 6. Git hooks arrive",
26
30
  "// as a `git` first word and route to the engine like hook words do, so",
@@ -28,7 +32,7 @@ export const LAUNCHER_TEXT = [
28
32
  "// Version arbitration",
29
33
  "// between the root and tool-dir engines (SPEC §4.19 EC-6) belongs to the",
30
34
  "// phase 9 CLI: until then the first candidate that exists wins.",
31
- launcherImport("import { existsSync }", "fs"),
35
+ launcherImport("import { existsSync, readFileSync }", "fs"),
32
36
  launcherImport("import path", "path"),
33
37
  launcherImport("import { fileURLToPath, pathToFileURL }", "url"),
34
38
  "",
@@ -141,6 +145,48 @@ export const LAUNCHER_TEXT = [
141
145
  "\treturn null;",
142
146
  "}",
143
147
  "",
148
+ "// SPEC §4.2.1 EC-11: the engine entry validates config.codictator",
149
+ "// against the installed engine version, so the launcher extracts it",
150
+ "// from the resolved engine's own manifest text and passes it as",
151
+ "// main's third argument. A version-field scan keeps JSON parsing in",
152
+ "// the kernel per §10.3 item 6 (and a regex literal would confuse the",
153
+ "// blankCode lexer the ts/lint gate runs on); any unreadable or",
154
+ "// version-less manifest answers null and the caller falls back to",
155
+ "// the two-argument call, which keeps the pinned placeholder —",
156
+ "// fail-closed downstream, never an allow.",
157
+ "function installedEngineVersion(enginePath) {",
158
+ "\ttry {",
159
+ "\t\tconst manifestPath = path.join(",
160
+ "\t\t\tpath.dirname(path.dirname(path.dirname(enginePath))),",
161
+ `\t\t\t"package.json",`,
162
+ "\t\t);",
163
+ `\t\tconst text = readFileSync(manifestPath, "utf8");`,
164
+ '\t\tconst marker = "\\"version\\":";',
165
+ "\t\tconst at = text.indexOf(marker);",
166
+ "\t\tif (at === -1) {",
167
+ "\t\t\treturn null;",
168
+ "\t\t}",
169
+ "\t\tlet index = at + marker.length;",
170
+ '\t\twhile (text[index] === " " || text[index] === "\\t" || text[index] === "\\n" || text[index] === "\\r") {',
171
+ "\t\t\tindex += 1;",
172
+ "\t\t}",
173
+ '\t\tif (text[index] !== "\\"") {',
174
+ "\t\t\treturn null;",
175
+ "\t\t}",
176
+ '\t\tconst end = text.indexOf("\\"", index + 1);',
177
+ "\t\tif (end === -1) {",
178
+ "\t\t\treturn null;",
179
+ "\t\t}",
180
+ "\t\tconst version = text.slice(index + 1, end);",
181
+ '\t\tif (version === "") {',
182
+ "\t\t\treturn null;",
183
+ "\t\t}",
184
+ "\t\treturn version;",
185
+ "\t} catch (ignored) {",
186
+ "\t\treturn null;",
187
+ "\t}",
188
+ "}",
189
+ "",
144
190
  "function reportMissing(harness) {",
145
191
  "\tconst missing = staticReply(",
146
192
  "\t\tharness,",
@@ -175,7 +221,11 @@ export const LAUNCHER_TEXT = [
175
221
  `\t\t\t\tif (engine === null || typeof engine.main !== "function") {`,
176
222
  `\t\t\t\t\tthrow new Error("engine entry has no main");`,
177
223
  "\t\t\t\t}",
178
- "\t\t\t\treturn engine.main(args, process.cwd());",
224
+ "\t\t\t\tconst version = installedEngineVersion(enginePath);",
225
+ "\t\t\t\tif (version === null) {",
226
+ "\t\t\t\t\treturn engine.main(args, process.cwd());",
227
+ "\t\t\t\t}",
228
+ "\t\t\t\treturn engine.main(args, process.cwd(), version);",
179
229
  "\t\t\t},",
180
230
  "\t\t\t(error) => {",
181
231
  "\t\t\t\tthrow error;",
@@ -6,6 +6,7 @@ import { exitFor } from "./shared.js";
6
6
  // these before invoking anything.
7
7
  const ALWAYS_HUMAN = new Set([
8
8
  "init",
9
+ "setup",
9
10
  "uninstall",
10
11
  "waive",
11
12
  ]);
@@ -0,0 +1,7 @@
1
+ import type { Result } from "../kernel/index.ts";
2
+ import type { SetupStep } from "./setup.ts";
3
+ import type { CommandRequest } from "./shared.ts";
4
+ export declare function ensureGitignoreEntries(root: string): Promise<Result<{
5
+ added: Array<string>;
6
+ }>>;
7
+ export declare function setupGitignoreStep(req: CommandRequest, root: string, steps: Array<SetupStep>): Promise<Result<number>>;
@@ -0,0 +1,41 @@
1
+ import { err, ok } from "../kernel/index.js";
2
+ import { fail, printHuman, readStateText, writeStateText } from "./shared.js";
3
+ const SETUP_GITIGNORE_ENTRIES = [
4
+ ".codictator/state/",
5
+ ".codictator/cache/",
6
+ ".codictator/tool/node_modules/",
7
+ ];
8
+ export async function ensureGitignoreEntries(root) {
9
+ const existing = await readStateText(root, ".gitignore");
10
+ const present = new Set((existing ?? "").split("\n").map((line) => line.trim()));
11
+ const missing = SETUP_GITIGNORE_ENTRIES.filter((entry) => !present.has(entry));
12
+ if (missing.length === 0) {
13
+ return ok({ added: [] });
14
+ }
15
+ const base = existing === undefined || existing === ""
16
+ ? ""
17
+ : existing.endsWith("\n")
18
+ ? existing
19
+ : `${existing}\n`;
20
+ const saved = await writeStateText(root, ".gitignore", `${base}${missing.join("\n")}\n`);
21
+ if (!saved.ok) {
22
+ return err(saved.error);
23
+ }
24
+ return ok({ added: [...missing] });
25
+ }
26
+ // SPEC §4.19.6 step 3: guarantee the manifest-owned .gitignore entries.
27
+ export async function setupGitignoreStep(req, root, steps) {
28
+ const ensured = await ensureGitignoreEntries(root);
29
+ if (!ensured.ok) {
30
+ return fail("CDX-E903", ensured.error.message, ensured.error.remediation, req.json);
31
+ }
32
+ steps.push({
33
+ step: "gitignore",
34
+ ok: true,
35
+ detail: { added: ensured.value.added },
36
+ });
37
+ if (ensured.value.added.length > 0) {
38
+ printHuman(`setup: added ${String(ensured.value.added.length)} .gitignore entries`, req.quiet);
39
+ }
40
+ return ok(0);
41
+ }
@@ -0,0 +1,7 @@
1
+ import type { InitFlags } from "./init-plan.ts";
2
+ export declare const SETUP_EXTENSION_ID = "codictator.codictator-vscode";
3
+ export declare const SETUP_EXTENSION_INSTALL = "code --install-extension codictator.codictator-vscode";
4
+ export declare const SETUP_PACK_REPAIR = "npx codictator setup --repair --pack typescript";
5
+ export declare const SETUP_PACK_FRESH = "npx codictator setup --pack typescript";
6
+ export declare function setupHandoffLines(emptyPacks: boolean): Array<string>;
7
+ export declare function hasEffectivePacks(root: string, flags: InitFlags): Promise<boolean>;
@@ -0,0 +1,44 @@
1
+ // SPEC §4.19.6 step 8: pack-less handoff text. Split from setup.ts
2
+ // (orchestration) by responsibility so setup.ts stays below the
3
+ // core/file-size 300 cap; the effective pack list mirrors the plan data.
4
+ import { parseJson } from "../kernel/index.js";
5
+ import { readStateText } from "./shared.js";
6
+ // The extension empty-state lives on the vscode branch (§4.20.1);
7
+ // handoff here only prints the install command.
8
+ export const SETUP_EXTENSION_ID = "codictator.codictator-vscode";
9
+ export const SETUP_EXTENSION_INSTALL = "code --install-extension codictator.codictator-vscode";
10
+ export const SETUP_PACK_REPAIR = "npx codictator setup --repair --pack typescript";
11
+ export const SETUP_PACK_FRESH = "npx codictator setup --pack typescript";
12
+ export function setupHandoffLines(emptyPacks) {
13
+ if (!emptyPacks) {
14
+ return [
15
+ "setup complete: open this folder in VS Code to see the Codictator views",
16
+ `extension: ${SETUP_EXTENSION_INSTALL}`,
17
+ ];
18
+ }
19
+ return [
20
+ "Setup complete — core rules only. No language packs installed.",
21
+ "To add a language pack, run exactly one line and press Enter:",
22
+ ` ${SETUP_PACK_REPAIR}`,
23
+ "To install the VS Code extension, run:",
24
+ ` ${SETUP_EXTENSION_INSTALL}`,
25
+ `Fresh adoption omits --repair: ${SETUP_PACK_FRESH}`,
26
+ ];
27
+ }
28
+ // SPEC §4.19.6 step 8: the effective pack list mirrors the plan data:
29
+ // explicit --pack flags win, otherwise the written config owns it.
30
+ export async function hasEffectivePacks(root, flags) {
31
+ if (flags.packs.length > 0) {
32
+ return true;
33
+ }
34
+ const text = await readStateText(root, ".codictator/config.json");
35
+ if (text === undefined) {
36
+ return false;
37
+ }
38
+ const parsed = parseJson(text);
39
+ if (!parsed.ok || typeof parsed.value !== "object" || parsed.value === null) {
40
+ return false;
41
+ }
42
+ const packs = parsed.value.packs;
43
+ return Array.isArray(packs) && packs.length > 0;
44
+ }
@@ -0,0 +1,11 @@
1
+ import type { Result } from "../kernel/index.ts";
2
+ import type { SetupStep } from "./setup.ts";
3
+ import type { CommandRequest } from "./shared.ts";
4
+ export interface SetupInstallTarget {
5
+ readonly dir: string;
6
+ readonly lockRel: string;
7
+ readonly dirRel: string;
8
+ }
9
+ export declare function setupInstallLayout(req: CommandRequest, root: string, steps: Array<SetupStep>): Promise<Result<SetupInstallTarget>>;
10
+ export declare function setupNpmArgv(platform?: string, execPath?: string): ReadonlyArray<string>;
11
+ export declare function setupInstallHere(req: CommandRequest, root: string, target: SetupInstallTarget, steps: Array<SetupStep>): Promise<Result<number>>;
@@ -0,0 +1,84 @@
1
+ // SPEC §4.19.4 + §4.19.6 steps 4-5: the engine install layout and the
2
+ // clean install here. npm repos install at the root; other repos get
3
+ // .codictator/tool/package.json pinning the engine exactly, then npm ci
4
+ // runs in the directory owning the lockfile. A registry or network outage
5
+ // maps to CDX-E401 (exit 5); any other install failure is a missing
6
+ // engine, CDX-E003 (exit 6).
7
+ import path from "node:path";
8
+ import { ENGINE_VERSION } from "../features/config/index.js";
9
+ import { err, ok, stableStringify } from "../kernel/index.js";
10
+ import { fail, portsFor, printHuman, readStateText, writeStateText, } from "./shared.js";
11
+ export async function setupInstallLayout(req, root, steps) {
12
+ const rootManifest = await readStateText(root, "package.json");
13
+ if (rootManifest !== undefined) {
14
+ steps.push({ step: "install", ok: true, detail: { dir: "." } });
15
+ return ok({ dir: root, lockRel: "package-lock.json", dirRel: "." });
16
+ }
17
+ const toolManifest = await readStateText(root, ".codictator/tool/package.json");
18
+ if (toolManifest === undefined) {
19
+ const engineVersion = req.engineVersion ?? ENGINE_VERSION;
20
+ const saved = await writeStateText(root, ".codictator/tool/package.json", `${stableStringify({
21
+ name: "codictator-tool",
22
+ private: true,
23
+ dependencies: { codictator: engineVersion },
24
+ })}\n`);
25
+ if (!saved.ok) {
26
+ return err(saved.error);
27
+ }
28
+ }
29
+ steps.push({
30
+ step: "install",
31
+ ok: true,
32
+ detail: { dir: ".codictator/tool" },
33
+ });
34
+ return ok({
35
+ dir: `${root}/.codictator/tool`,
36
+ lockRel: ".codictator/tool/package-lock.json",
37
+ dirRel: ".codictator/tool",
38
+ });
39
+ }
40
+ function looksLikeRegistryOutage(output) {
41
+ return /ENOTFOUND|EAI_AGAIN|ETIMEDOUT|ECONNRESET|ECONNREFUSED|registry/i.test(output);
42
+ }
43
+ // SPEC §4.1.1: the child spawns with shell:false always, so on win32 the
44
+ // bare `npm` shim never resolves: it is an npm.cmd batch file, which the
45
+ // shell-less spawn cannot execute (ENOENT without, EINVAL with, the
46
+ // extension). The fix stays shell-free by exec'ing the real entry beside
47
+ // the running node: node <node-dir>/node_modules/npm/bin/npm-cli.js ci.
48
+ // POSIX keeps `npm` from PATH. Pure in platform and execPath so tests pin
49
+ // the exact proc argv without spawning anything.
50
+ export function setupNpmArgv(platform = process.platform, execPath = process.execPath) {
51
+ if (platform !== "win32") {
52
+ return ["npm", "ci"];
53
+ }
54
+ return [
55
+ execPath,
56
+ path.join(path.dirname(execPath), "node_modules", "npm", "bin", "npm-cli.js"),
57
+ "ci",
58
+ ];
59
+ }
60
+ export async function setupInstallHere(req, root, target, steps) {
61
+ const lock = await readStateText(root, target.lockRel);
62
+ if (lock === undefined) {
63
+ return fail("CDX-E003", `no lockfile at ${target.lockRel}, so npm ci cannot run`, `Run npm install in ${target.dirRel} (needs network access), then rerun codictator setup.`, req.json);
64
+ }
65
+ const ports = await portsFor(root);
66
+ if (!ports.ok) {
67
+ return fail("CDX-E903", ports.error.message, ports.error.remediation, req.json);
68
+ }
69
+ const ran = await ports.value.proc.run(setupNpmArgv(), {
70
+ cwd: target.dir,
71
+ env: {},
72
+ timeoutMs: 300000,
73
+ maxOutputBytes: 4 * 1024 * 1024,
74
+ });
75
+ if (!ran.timedOut && !ran.truncated && ran.code === 0) {
76
+ steps.push({ step: "install-here", ok: true });
77
+ printHuman(`setup: npm ci passed in ${target.dirRel}`, req.quiet);
78
+ return ok(0);
79
+ }
80
+ if (ran.timedOut || looksLikeRegistryOutage(`${ran.stdout}\n${ran.stderr}`)) {
81
+ return fail("CDX-E401", "registry or network unavailable during install", "Retry with network access, then rerun codictator setup.", req.json);
82
+ }
83
+ return fail("CDX-E003", `engine install failed in ${target.dirRel} (npm ci exit ${String(ran.code)})`, `Run npm ci in ${target.dirRel}, then rerun codictator setup.`, req.json);
84
+ }
@@ -0,0 +1,9 @@
1
+ import type { Result } from "../kernel/index.ts";
2
+ import type { CommandRequest } from "./shared.ts";
3
+ export declare const SETUP_USAGE = "usage: codictator setup [--harness <id> ...] [--profile <app|library|cli|service>] [--pack <id> ...] [--tool <role>=<command>@<pin> ...] [--repair]";
4
+ export interface SetupStep {
5
+ readonly step: string;
6
+ readonly ok: boolean;
7
+ readonly detail?: Record<string, unknown>;
8
+ }
9
+ export declare function handleSetup(req: CommandRequest): Promise<Result<number>>;