peaks-loop 4.0.16 → 4.0.18

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 (80) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README-en.md +12 -9
  3. package/README.md +15 -4
  4. package/config/eslint/.peaks-rules.cjs +39 -26
  5. package/dist/cli/commands/_register.js +2 -1
  6. package/dist/cli/commands/audit-commands.d.ts +1 -1
  7. package/dist/cli/commands/code-gate-command.d.ts +25 -0
  8. package/dist/cli/commands/code-gate-command.js +86 -0
  9. package/dist/cli/commands/code-mode-gate-commands.js +3 -4
  10. package/dist/cli/commands/code-orchestrator-can-do.js +2 -1
  11. package/dist/cli/commands/cron-commands.js +16 -4
  12. package/dist/cli/commands/dashboard-long-run.js +8 -4
  13. package/dist/cli/commands/dispatch-from-dag.d.ts +1 -1
  14. package/dist/cli/commands/hooks-commands.js +56 -3
  15. package/dist/cli/commands/primer-command.d.ts +59 -0
  16. package/dist/cli/commands/primer-command.js +160 -0
  17. package/dist/cli/commands/qa-commands.js +1 -2
  18. package/dist/cli/commands/sediment-commands.d.ts +1 -1
  19. package/dist/cli/commands/slice-commands.js +20 -8
  20. package/dist/cli/commands/statusline-commands.d.ts +1 -0
  21. package/dist/cli/commands/statusline-commands.js +17 -3
  22. package/dist/cli/commands/tech-commands.d.ts +1 -1
  23. package/dist/cli/program.js +8 -0
  24. package/dist/services/artifacts/request-artifact-service.d.ts +2 -2
  25. package/dist/services/artifacts/request-artifact-service.js +10 -1
  26. package/dist/services/audit/enforcers/lint-reference-shape.js +18 -4
  27. package/dist/services/code/auto-compact-orchestrator.js +9 -1
  28. package/dist/services/code/batch-heartbeat-poller.js +26 -0
  29. package/dist/services/code/orchestrator-can-do.d.ts +24 -0
  30. package/dist/services/code/orchestrator-can-do.js +48 -2
  31. package/dist/services/code/post-compact-detector.d.ts +1 -1
  32. package/dist/services/compact-statusline/compact-statusline-service.js +19 -11
  33. package/dist/services/config/config-safety.d.ts +23 -0
  34. package/dist/services/config/config-safety.js +62 -0
  35. package/dist/services/config/config-service.d.ts +1 -1
  36. package/dist/services/config/config-service.js +2 -4
  37. package/dist/services/container/container-lease.js +21 -8
  38. package/dist/services/context/spillover-store.d.ts +1 -1
  39. package/dist/services/crystallization/crystallization-types.js +20 -8
  40. package/dist/services/crystallization/evidence-brief-builder.js +22 -11
  41. package/dist/services/dispatch/dispatch-record-writer.js +231 -141
  42. package/dist/services/evolution/evolution-types.js +38 -21
  43. package/dist/services/feedback/feedback-promotion-service.js +9 -1
  44. package/dist/services/hooks/pre-tool-code-gate.d.ts +46 -0
  45. package/dist/services/hooks/pre-tool-code-gate.js +91 -0
  46. package/dist/services/ide/current-model-detector.js +1 -2
  47. package/dist/services/lint/detect-eslint.js +8 -5
  48. package/dist/services/lint/eslint-runner.d.ts +1 -2
  49. package/dist/services/lint/eslint-runner.js +86 -23
  50. package/dist/services/lint/npx-resolver.d.ts +6 -0
  51. package/dist/services/lint/npx-resolver.js +47 -0
  52. package/dist/services/loop/loop-release-types.js +32 -13
  53. package/dist/services/loop/spec-service.d.ts +7 -1
  54. package/dist/services/loop/spec-service.js +218 -182
  55. package/dist/services/rd/rd-service.js +22 -9
  56. package/dist/services/scan/archetype-service.js +21 -9
  57. package/dist/services/session/binding-store.js +1 -2
  58. package/dist/services/share/bundle-reader.d.ts +6 -9
  59. package/dist/services/share/bundle-reader.js +297 -223
  60. package/dist/services/skills/hooks-settings-service.d.ts +33 -0
  61. package/dist/services/skills/hooks-settings-service.js +48 -3
  62. package/dist/services/skills/{outer-cache-hook-constants.d.ts → session-start-hook-constants.d.ts} +20 -0
  63. package/dist/services/skills/{outer-cache-hook-constants.js → session-start-hook-constants.js} +20 -0
  64. package/dist/services/skills/skill-statusline-renderer.d.ts +50 -1
  65. package/dist/services/skills/skill-statusline-renderer.js +96 -8
  66. package/dist/services/skills/skill-statusline-service.d.ts +41 -0
  67. package/dist/services/skills/skill-statusline-service.js +56 -6
  68. package/dist/services/slice/calibration-store.js +21 -8
  69. package/dist/services/slice/slice-check-service.js +35 -14
  70. package/dist/services/slice/slice-decompose-service.js +459 -223
  71. package/dist/services/standards/project-context.js +135 -73
  72. package/dist/services/verdict/envelopes.d.ts +1 -2
  73. package/dist/services/verdict/envelopes.js +1 -2
  74. package/dist/services/verdict/verdict-aggregator.d.ts +1 -2
  75. package/dist/services/vm/vm-lease.js +19 -8
  76. package/dist/services/worktree/worktree-lease.d.ts +0 -10
  77. package/dist/services/worktree/worktree-lease.js +19 -8
  78. package/dist/shared/fs-utils.d.ts +1 -1
  79. package/package.json +7 -4
  80. package/skills/peaks-code/SKILL.md +46 -0
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Slice 2026-08-06-codegate-vendor-neutral — Code-Gate core logic.
3
+ *
4
+ * Vendor-neutral PreToolUse gate. Reads the standard `{tool, input}`
5
+ * JSON hook payload (any IDE / harness using this protocol is
6
+ * supported; the implementation is host-agnostic) and decides
7
+ * whether the tool call is allowed:
8
+ *
9
+ * Edit / Write / MultiEdit on hard-blocked path families
10
+ * (src/, tests/unit/, tests/integration/, config/, bin/, scripts/)
11
+ * → deny with `PEAKS_CODE_PROHIBITED_DIRECT_EDIT`.
12
+ *
13
+ * All other tool calls, OR allow-listed paths
14
+ * (.peaks/**, .peaks/_runtime/**, skills/**, docs/**, *.md)
15
+ * → allow (silent exit 0).
16
+ *
17
+ * Pure function. The shell-script sibling
18
+ * (`pre-tool-code-gate.sh`) is the canonical source for the same
19
+ * logic, usable from any non-Node harness. Both share the same
20
+ * hard-blocked + allow-listed path families.
21
+ */
22
+ export type GateToolName = 'Edit' | 'Write' | 'MultiEdit' | string;
23
+ export type GatePathKey = 'file_path' | 'path' | 'notebook_path';
24
+ export interface GateInput {
25
+ readonly tool: GateToolName;
26
+ readonly input: Readonly<Record<string, unknown>>;
27
+ }
28
+ /** Hard-blocked path families — orchestrator MUST NOT Edit/Write these. */
29
+ export declare const HARD_BLOCKED_PATH_FAMILIES: readonly ["src/", "tests/unit/", "tests/integration/", "config/", "bin/", "scripts/"];
30
+ /** Allow-listed path prefixes / suffixes — orchestrator may freely Edit/Write these. */
31
+ export declare const ALLOW_LISTED_PATH_PATTERNS: readonly [".peaks/", ".peaks_", "skills/", "docs/", ".md", "CHANGELOG.md", "README.md"];
32
+ export type GateVerdict = {
33
+ readonly action: 'allow';
34
+ } | {
35
+ readonly action: 'deny';
36
+ readonly filePath: string;
37
+ readonly reason: string;
38
+ readonly message: string;
39
+ };
40
+ /** Pull the target file path from `input.file_path | input.path | input.notebook_path`. */
41
+ export declare function extractFilePath(input: Readonly<Record<string, unknown>>): string;
42
+ /**
43
+ * Pure decision function. Given the tool name + input object, return
44
+ * the gate verdict. Tolerates malformed payloads (allow).
45
+ */
46
+ export declare function decideGateAction(tool: GateToolName, input: Readonly<Record<string, unknown>>): GateVerdict;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Slice 2026-08-06-codegate-vendor-neutral — Code-Gate core logic.
3
+ *
4
+ * Vendor-neutral PreToolUse gate. Reads the standard `{tool, input}`
5
+ * JSON hook payload (any IDE / harness using this protocol is
6
+ * supported; the implementation is host-agnostic) and decides
7
+ * whether the tool call is allowed:
8
+ *
9
+ * Edit / Write / MultiEdit on hard-blocked path families
10
+ * (src/, tests/unit/, tests/integration/, config/, bin/, scripts/)
11
+ * → deny with `PEAKS_CODE_PROHIBITED_DIRECT_EDIT`.
12
+ *
13
+ * All other tool calls, OR allow-listed paths
14
+ * (.peaks/**, .peaks/_runtime/**, skills/**, docs/**, *.md)
15
+ * → allow (silent exit 0).
16
+ *
17
+ * Pure function. The shell-script sibling
18
+ * (`pre-tool-code-gate.sh`) is the canonical source for the same
19
+ * logic, usable from any non-Node harness. Both share the same
20
+ * hard-blocked + allow-listed path families.
21
+ */
22
+ /** Hard-blocked path families — orchestrator MUST NOT Edit/Write these. */
23
+ export const HARD_BLOCKED_PATH_FAMILIES = [
24
+ 'src/',
25
+ 'tests/unit/',
26
+ 'tests/integration/',
27
+ 'config/',
28
+ 'bin/',
29
+ 'scripts/',
30
+ ];
31
+ /** Allow-listed path prefixes / suffixes — orchestrator may freely Edit/Write these. */
32
+ export const ALLOW_LISTED_PATH_PATTERNS = [
33
+ '.peaks/',
34
+ '.peaks_',
35
+ 'skills/',
36
+ 'docs/',
37
+ '.md',
38
+ 'CHANGELOG.md',
39
+ 'README.md',
40
+ ];
41
+ /** Pull the target file path from `input.file_path | input.path | input.notebook_path`. */
42
+ export function extractFilePath(input) {
43
+ for (const key of ['file_path', 'path', 'notebook_path']) {
44
+ const v = input[key];
45
+ if (typeof v === 'string' && v.length > 0)
46
+ return v;
47
+ }
48
+ return '';
49
+ }
50
+ function matchesAny(path, families) {
51
+ for (const fam of families) {
52
+ if (path.startsWith(fam))
53
+ return fam;
54
+ // also handle ".md" suffix pattern
55
+ if (fam.startsWith('.') && path.endsWith(fam))
56
+ return fam;
57
+ }
58
+ return null;
59
+ }
60
+ /**
61
+ * Pure decision function. Given the tool name + input object, return
62
+ * the gate verdict. Tolerates malformed payloads (allow).
63
+ */
64
+ export function decideGateAction(tool, input) {
65
+ // Only gate Edit / Write / MultiEdit.
66
+ if (tool !== 'Edit' && tool !== 'Write' && tool !== 'MultiEdit') {
67
+ return { action: 'allow' };
68
+ }
69
+ const filePath = extractFilePath(input);
70
+ if (filePath.length === 0) {
71
+ // No path → cannot decide; fail-open (allow). The probe side
72
+ // (`orchestrator-can-do`) handles slice-spec content; this hook
73
+ // handles file paths only.
74
+ return { action: 'allow' };
75
+ }
76
+ // Allow-list check first — short-circuits any deny.
77
+ const allowMatch = matchesAny(filePath, ALLOW_LISTED_PATH_PATTERNS);
78
+ if (allowMatch !== null) {
79
+ return { action: 'allow' };
80
+ }
81
+ // Hard-blocked family check.
82
+ const denyMatch = matchesAny(filePath, HARD_BLOCKED_PATH_FAMILIES);
83
+ if (denyMatch !== null) {
84
+ const reason = `${denyMatch} (orchestrator's hard-blocked path family)`;
85
+ const message = `PEAKS_CODE_PROHIBITED_DIRECT_EDIT: ${filePath} matches hard-blocked path family ${reason}; ` +
86
+ `orchestrator MUST NOT Edit/Write these directly. Use: peaks sub-agent dispatch rd --prompt '<your task>' --request-id <rid> --project . --batch-id <uuid>`;
87
+ return { action: 'deny', filePath, reason, message };
88
+ }
89
+ // Anything not in the deny list → allow (e.g. CHANGELOG, README, top-level files).
90
+ return { action: 'allow' };
91
+ }
@@ -24,8 +24,7 @@
24
24
  * - It returns `undefined` when detection is unavailable; callers
25
25
  * must fall back to the configured / hardcoded model.
26
26
  */
27
- import { getAdapter } from './ide-registry.js';
28
- import { listAdapterIds } from './ide-registry.js';
27
+ import { getAdapter, listAdapterIds } from './ide-registry.js';
29
28
  /**
30
29
  * Probe every registered IDE adapter for its currently-active model
31
30
  * id. Returns the first non-empty value found in registration order,
@@ -4,29 +4,32 @@
4
4
  * unified Gate B5 verdict.
5
5
  */
6
6
  import { spawnSync } from 'node:child_process';
7
+ import { resolveNpxInvocation } from './npx-resolver.js';
7
8
  import { ESLINT_PACKAGE_PINS } from './eslint-runner.js';
8
9
  const PACKAGES_TO_PROBE = [
9
10
  'eslint',
10
11
  'typescriptEslintParser',
11
- 'typescriptEslintPlugin',
12
- 'importPlugin'
12
+ 'typescriptEslintPlugin'
13
13
  ];
14
14
  function packageNameFor(key) {
15
15
  switch (key) {
16
16
  case 'eslint': return 'eslint';
17
17
  case 'typescriptEslintParser': return '@typescript-eslint/parser';
18
18
  case 'typescriptEslintPlugin': return '@typescript-eslint/eslint-plugin';
19
- case 'importPlugin': return 'eslint-plugin-import';
20
19
  }
21
20
  }
22
21
  function probeNpx() {
23
- const probe = spawnSync('npx', ['--version'], { encoding: 'utf8' });
22
+ const { command, args, baseEnv } = resolveNpxInvocation(['--version']);
23
+ const probe = spawnSync(command, args, { encoding: 'utf8', env: baseEnv });
24
24
  return probe.status === 0;
25
25
  }
26
26
  function probePackage(key) {
27
27
  const pkg = packageNameFor(key);
28
28
  const pin = ESLINT_PACKAGE_PINS[key];
29
- const result = spawnSync('npm', ['view', `${pkg}@${pin}`, 'version'], { encoding: 'utf8' });
29
+ // shell:true required on Windows because `npm` (like npx) is a `.cmd`
30
+ // shim; Node 22 refuses to invoke it without shell wrapper.
31
+ // 2026-08-06 lint-dogfood cycle-2 follow-up.
32
+ const result = spawnSync('npm', ['view', `${pkg}@${pin}`, 'version'], { encoding: 'utf8', shell: true });
30
33
  return result.status === 0;
31
34
  }
32
35
  export function detectEslint() {
@@ -1,8 +1,7 @@
1
1
  export declare const ESLINT_PACKAGE_PINS: {
2
- readonly eslint: "10.8.0";
2
+ readonly eslint: "8.57.1";
3
3
  readonly typescriptEslintParser: "8.66.0";
4
4
  readonly typescriptEslintPlugin: "8.66.0";
5
- readonly importPlugin: "2.32.0";
6
5
  };
7
6
  export type RedLineMode = 'none' | 'baseline-aware';
8
7
  export type EslintState = 'ok' | 'eslint-missing' | 'npx-failed' | 'execution-failed' | 'baseline-missing';
@@ -18,18 +18,34 @@
18
18
  * red-line. Enforces D6 + supplementary S2.
19
19
  */
20
20
  import { spawnSync } from 'node:child_process';
21
- import { readFileSync } from 'node:fs';
22
- import { join } from 'node:path';
21
+ import { existsSync, readFileSync } from 'node:fs';
22
+ import { dirname, join } from 'node:path';
23
+ import { resolveNpxInvocation } from './npx-resolver.js';
24
+ /**
25
+ * PRD-002b slice 2 — extract runner-pipeline magic numbers (ESLint
26
+ * severity codes, buffer / timeout budgets, max project-root walk
27
+ * depth, red-line top-N aggregation cap, base severity defaults).
28
+ * Values are bytewise-identical to the original literals.
29
+ */
30
+ const ESLINT_SEVERITY_ERROR = 2;
31
+ const ESLINT_SEVERITY_WARN = 1;
32
+ const ESLINT_DEFAULT_TIMEOUT_MS = 60_000;
33
+ const KB_PER_MB = 1024;
34
+ const BYTES_PER_KB = 1024;
35
+ const MB_TO_BYTES = KB_PER_MB * BYTES_PER_KB;
36
+ const DIFF_BUFFER_BYTES = 16 * MB_TO_BYTES;
37
+ const OUTPUT_BUFFER_BYTES = 32 * MB_TO_BYTES;
38
+ const PROJECT_ROOT_WALK_MAX_DEPTH = 8;
39
+ const RED_LINE_TOP_FILES = 5;
23
40
  export const ESLINT_PACKAGE_PINS = {
24
- eslint: '10.8.0',
41
+ eslint: '8.57.1',
25
42
  typescriptEslintParser: '8.66.0',
26
- typescriptEslintPlugin: '8.66.0',
27
- importPlugin: '2.32.0'
43
+ typescriptEslintPlugin: '8.66.0'
28
44
  };
29
45
  function severityFor(value) {
30
- if (value === 2)
46
+ if (value === ESLINT_SEVERITY_ERROR)
31
47
  return 'error';
32
- if (value === 1)
48
+ if (value === ESLINT_SEVERITY_WARN)
33
49
  return 'warn';
34
50
  return 'info';
35
51
  }
@@ -52,13 +68,30 @@ function summarize(findings) {
52
68
  * touched line number. Falls back to [] on any parse error so the
53
69
  * caller treats all findings as out-of-diff (no silent zero-result).
54
70
  */
71
+ function resolveProjectRoot(cwd) {
72
+ // ESLint 8 auto-discovers `.eslintrc.*` from cwd upward. When the
73
+ // CLI is launched via `node bin/peaks.js`, cwd is the bin/ dir and
74
+ // ESLint fails to find the config. Walk up until we see
75
+ // `config/eslint/.peaks-rules.cjs` and use that as the project root.
76
+ const marker = join('config', 'eslint', '.peaks-rules.cjs');
77
+ let current = cwd;
78
+ for (let depth = 0; depth < PROJECT_ROOT_WALK_MAX_DEPTH; depth += 1) {
79
+ if (existsSync(join(current, marker)))
80
+ return current;
81
+ const parent = dirname(current);
82
+ if (parent === current)
83
+ break;
84
+ current = parent;
85
+ }
86
+ return cwd;
87
+ }
55
88
  function loadDiffRanges(cwd) {
56
89
  const ranges = [];
57
90
  try {
58
91
  const result = spawnSync('git', ['diff', 'HEAD', '--unified=0', '--no-color'], {
59
92
  cwd,
60
93
  encoding: 'utf8',
61
- maxBuffer: 16 * 1024 * 1024
94
+ maxBuffer: DIFF_BUFFER_BYTES
62
95
  });
63
96
  if (result.status !== 0 || typeof result.stdout !== 'string')
64
97
  return [];
@@ -168,7 +201,7 @@ function aggregateRedLine(baseline) {
168
201
  const topFiles = [...agg.fileCounts.entries()]
169
202
  .map(([file, count]) => ({ file, count }))
170
203
  .sort((a, b) => b.count - a.count)
171
- .slice(0, 5);
204
+ .slice(0, RED_LINE_TOP_FILES);
172
205
  out.push({ ruleId, count: agg.count, topFiles });
173
206
  }
174
207
  out.sort((a, b) => b.count - a.count);
@@ -193,16 +226,19 @@ export function buildEslintArgs(options) {
193
226
  code: 'LINT_FIX_FORBIDDEN'
194
227
  });
195
228
  }
196
- const args = [
197
- '--package', `eslint@${ESLINT_PACKAGE_PINS.eslint}`,
198
- '--package', `@typescript-eslint/parser@${ESLINT_PACKAGE_PINS.typescriptEslintParser}`,
199
- '--package', `@typescript-eslint/eslint-plugin@${ESLINT_PACKAGE_PINS.typescriptEslintPlugin}`,
200
- '--package', `eslint-plugin-import@${ESLINT_PACKAGE_PINS.importPlugin}`,
201
- '--', 'eslint', '--no-warn-ignored', '--format', 'json'
202
- ];
203
- if (options.configPath !== undefined) {
204
- args.push('--config', options.configPath);
205
- }
229
+ // The runner now uses the locally-installed eslint binary
230
+ // (`./node_modules/eslint/bin/eslint.js`) instead of the npx
231
+ // --package wrapper, which is broken on Windows (npm 10.9.4 chdirs
232
+ // the child to its own cache bin, breaking config auto-discovery).
233
+ // The pin constants are kept for detect-eslint's npm-registry
234
+ // probe + for the npx-resolver fallback path.
235
+ const args = ['--format', 'json'];
236
+ // Always pass the legacy .peaks-rules.cjs path; ESLint 8
237
+ // auto-discovers only `.eslintrc.*` files and our config lives at
238
+ // `config/eslint/.peaks-rules.cjs`. Callers may override via
239
+ // `options.configPath`.
240
+ const effectiveConfigPath = options.configPath ?? join('config', 'eslint', '.peaks-rules.cjs');
241
+ args.push('--config', effectiveConfigPath);
206
242
  args.push(...(options.scope !== undefined && options.scope.length > 0 ? [options.scope] : ['.']));
207
243
  return args;
208
244
  }
@@ -215,13 +251,40 @@ export function runEslint(options) {
215
251
  catch (error) {
216
252
  return emptyResult('execution-failed', start, error instanceof Error ? error.message : String(error));
217
253
  }
254
+ const projectRoot = resolveProjectRoot(options.cwd);
255
+ // Invoke eslint via `node <node_modules/eslint/bin/eslint.js>` to bypass
256
+ // the Windows .cmd shim entirely (Node 22 spawnSync cannot run .cmd
257
+ // shims without shell:true, and shell:true mangles quoted args).
258
+ const localEslintJs = join(projectRoot, 'node_modules', 'eslint', 'bin', 'eslint.js');
259
+ const useLocal = existsSync(localEslintJs);
218
260
  const spawnOptions = {
219
- cwd: options.cwd,
261
+ cwd: projectRoot,
220
262
  encoding: 'utf8',
221
- timeout: options.timeoutMs ?? 60_000,
222
- maxBuffer: 32 * 1024 * 1024
263
+ timeout: options.timeoutMs ?? ESLINT_DEFAULT_TIMEOUT_MS,
264
+ maxBuffer: OUTPUT_BUFFER_BYTES
223
265
  };
224
- const result = spawnSync('npx', args, spawnOptions);
266
+ let command;
267
+ let invocationArgs;
268
+ let baseEnv;
269
+ if (useLocal) {
270
+ command = process.execPath;
271
+ invocationArgs = [localEslintJs, ...args];
272
+ baseEnv = process.env;
273
+ }
274
+ else {
275
+ // Fallback: resolve `npx` through the user's bundled npm install to
276
+ // bypass the Windows .cmd shim + shell-quoting issues.
277
+ const resolved = resolveNpxInvocation([
278
+ '--package', `eslint@${ESLINT_PACKAGE_PINS.eslint}`,
279
+ '--package', `@typescript-eslint/parser@${ESLINT_PACKAGE_PINS.typescriptEslintParser}`,
280
+ '--package', `@typescript-eslint/eslint-plugin@${ESLINT_PACKAGE_PINS.typescriptEslintPlugin}`,
281
+ '--', 'eslint', ...args
282
+ ]);
283
+ command = resolved.command;
284
+ invocationArgs = resolved.args;
285
+ baseEnv = resolved.baseEnv;
286
+ }
287
+ const result = spawnSync(command, invocationArgs, { ...spawnOptions, env: baseEnv });
225
288
  if (result.error !== undefined && result.error !== null) {
226
289
  const message = result.error.message;
227
290
  const state = /ENOENT/.test(message) ? 'npx-failed' : 'execution-failed';
@@ -0,0 +1,6 @@
1
+ export type NpxInvocation = {
2
+ readonly command: string;
3
+ readonly args: readonly string[];
4
+ readonly baseEnv: NodeJS.ProcessEnv;
5
+ };
6
+ export declare function resolveNpxInvocation(npxArgs: readonly string[]): NpxInvocation;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Cross-platform `npx` resolver.
3
+ *
4
+ * On Windows, `npm` installs `npx` as a `.cmd` shim; Node 22's
5
+ * `child_process.spawnSync` refuses to invoke it unless `shell: true`
6
+ * is set, and `shell: true` corrupts quoted `--package` arguments.
7
+ * Rather than depend on shell quoting, this helper resolves the
8
+ * npx script bundled with the user's `npm` install and invokes it
9
+ * via `node <npx-cli.js>` with the same argv. macOS / Linux continue
10
+ * to use the regular `npx` binary.
11
+ */
12
+ import { existsSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import process from 'node:process';
15
+ function locateNpxCliScript() {
16
+ const candidates = process.platform === 'win32'
17
+ ? [
18
+ join(process.execPath, '..', '..', 'node_modules', 'npm', 'bin', 'npx-cli.js'),
19
+ 'C:/nvm4w/nodejs/node_modules/npm/bin/npx-cli.js',
20
+ 'C:/Program Files/nodejs/node_modules/npm/bin/npx-cli.js'
21
+ ]
22
+ : [
23
+ join(process.execPath, '..', '..', 'lib', 'node_modules', 'npm', 'bin', 'npx-cli.js')
24
+ ];
25
+ for (const candidate of candidates) {
26
+ if (existsSync(candidate))
27
+ return candidate;
28
+ }
29
+ return null;
30
+ }
31
+ export function resolveNpxInvocation(npxArgs) {
32
+ if (process.platform === 'win32') {
33
+ const cliScript = locateNpxCliScript();
34
+ if (cliScript !== null) {
35
+ return {
36
+ command: process.execPath,
37
+ args: [cliScript, ...npxArgs],
38
+ baseEnv: process.env
39
+ };
40
+ }
41
+ }
42
+ return {
43
+ command: 'npx',
44
+ args: npxArgs,
45
+ baseEnv: process.env
46
+ };
47
+ }
@@ -1,4 +1,23 @@
1
1
  import { z } from "zod";
2
+ /* ---------------------------------------------------------------------- */
3
+ /* PRD-002b slice 2 — schema-limit constants extracted from inline */
4
+ /* `.max(N)` calls so the no-magic-numbers rule stops flagging the */
5
+ /* constraint values. Names describe the field, not just the number. */
6
+ /* Bytewise-identical to the original literals (signaled in commit). */
7
+ /* ---------------------------------------------------------------------- */
8
+ const LOOP_ID_MAX = 64;
9
+ const LOOP_SHARE_EXCLUDED_PATH_MAX = 4096;
10
+ const LOOP_NAME_MAX = 200;
11
+ const LOOP_SCENARIO_MAX = 4000;
12
+ const LOOP_TRIGGER_POLICY_MAX = 2000;
13
+ const LOOP_SUCCESS_CRITERIA_ITEM_MAX = 1000;
14
+ const LOOP_INTERACTION_POLICY_MAX = 2000;
15
+ const LOOP_FEEDBACK_POLICY_MAX = 2000;
16
+ const LOOP_EVOLUTION_POLICY_MAX = 2000;
17
+ const LOOP_EVALUATOR_POLICY_ITEM_MAX = 1000;
18
+ const LOOP_LINKED_BEE_MAX = 64;
19
+ const LOOP_HISTORY_ITEM_MAX = 128;
20
+ const LOOP_CRYSTALLIZATION_EVIDENCE_MAX = 128;
2
21
  /**
3
22
  * LoopRelease — spec §4.1 (Loop Engineering Asset).
4
23
  *
@@ -74,7 +93,7 @@ export const LOOP_RELEASE_M3_FIELDS = [
74
93
  export const LoopReleaseM3ExtensionSchema = z.object({
75
94
  shareable: z.boolean().default(true),
76
95
  share_excluded_paths: z
77
- .array(z.string().min(1).max(4096))
96
+ .array(z.string().min(1).max(LOOP_SHARE_EXCLUDED_PATH_MAX))
78
97
  .default([]),
79
98
  desktop_visible: z.boolean().default(true),
80
99
  export_bundle_format: z
@@ -92,27 +111,27 @@ export const LoopReleaseInputSchema = z
92
111
  id: z
93
112
  .string()
94
113
  .min(1)
95
- .max(64)
114
+ .max(LOOP_ID_MAX)
96
115
  .regex(/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/, {
97
116
  message: "id must be kebab-case starting with a lowercase letter (e.g. loop-onboarding-research)",
98
117
  }),
99
- name: z.string().min(1).max(200),
100
- scenario: z.string().trim().min(1).max(4000),
101
- trigger_policy: z.string().trim().min(1).max(2000),
118
+ name: z.string().min(1).max(LOOP_NAME_MAX),
119
+ scenario: z.string().trim().min(1).max(LOOP_SCENARIO_MAX),
120
+ trigger_policy: z.string().trim().min(1).max(LOOP_TRIGGER_POLICY_MAX),
102
121
  success_criteria: z
103
- .array(z.string().trim().min(1).max(1000))
122
+ .array(z.string().trim().min(1).max(LOOP_SUCCESS_CRITERIA_ITEM_MAX))
104
123
  .min(1, "success_criteria must list at least one declarative criterion"),
105
124
  interaction_policy: z
106
125
  .string()
107
126
  .trim()
108
127
  .min(1, "interaction_policy must declare human-NL-choice-only semantics")
109
- .max(2000),
110
- feedback_policy: z.string().trim().min(1).max(2000),
111
- evolution_policy: z.string().trim().min(1).max(2000),
112
- evaluator_policy: z.array(z.string().trim().min(1).max(1000)).min(1),
113
- linked_bees: z.array(z.string().min(1).max(64)).default([]),
114
- run_history: z.array(z.string().min(1).max(128)).default([]),
115
- crystallization_evidence: z.array(z.string().min(1).max(128)).default([]),
128
+ .max(LOOP_INTERACTION_POLICY_MAX),
129
+ feedback_policy: z.string().trim().min(1).max(LOOP_FEEDBACK_POLICY_MAX),
130
+ evolution_policy: z.string().trim().min(1).max(LOOP_EVOLUTION_POLICY_MAX),
131
+ evaluator_policy: z.array(z.string().trim().min(1).max(LOOP_EVALUATOR_POLICY_ITEM_MAX)).min(1),
132
+ linked_bees: z.array(z.string().min(1).max(LOOP_LINKED_BEE_MAX)).default([]),
133
+ run_history: z.array(z.string().min(1).max(LOOP_HISTORY_ITEM_MAX)).default([]),
134
+ crystallization_evidence: z.array(z.string().min(1).max(LOOP_CRYSTALLIZATION_EVIDENCE_MAX)).default([]),
116
135
  lifecycle_status: LoopReleaseLifecycleStatusSchema,
117
136
  version: z
118
137
  .string()
@@ -57,7 +57,13 @@ export declare function resolveLoopSpec(projectRoot: string, sid: string, rid: s
57
57
  * `buildSpec` preserves raw values so out-of-range entries surface in
58
58
  * `lintLoopSpec` rather than being silently clamped. */
59
59
  export declare function buildSpec(input: Partial<LoopSpec>, expectedRid: string): LoopSpec;
60
- /** Lint a LoopSpec — returns a report with semantic errors / warnings. */
60
+ /** Lint a LoopSpec — returns a report with semantic errors / warnings.
61
+ *
62
+ * Refactored 2026-08-07 (PRD-002b slice 3 commit A — extract-method):
63
+ * the original function had complexity 14. Each validation phase now lives
64
+ * in its own helper, dropping the orchestrator to complexity ~5.
65
+ * Behavior is byte-for-byte preserved: same error/warning strings, same
66
+ * ordering, same return shape. */
61
67
  export declare function lintLoopSpec(spec: LoopSpec): SpecLintReport;
62
68
  /** Serialize a LoopSpec to a stable YAML representation. Pure. */
63
69
  export declare function serializeSpec(spec: LoopSpec): string;