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.
- package/CHANGELOG.md +55 -0
- package/README-en.md +12 -9
- package/README.md +15 -4
- package/config/eslint/.peaks-rules.cjs +39 -26
- package/dist/cli/commands/_register.js +2 -1
- package/dist/cli/commands/audit-commands.d.ts +1 -1
- package/dist/cli/commands/code-gate-command.d.ts +25 -0
- package/dist/cli/commands/code-gate-command.js +86 -0
- package/dist/cli/commands/code-mode-gate-commands.js +3 -4
- package/dist/cli/commands/code-orchestrator-can-do.js +2 -1
- package/dist/cli/commands/cron-commands.js +16 -4
- package/dist/cli/commands/dashboard-long-run.js +8 -4
- package/dist/cli/commands/dispatch-from-dag.d.ts +1 -1
- package/dist/cli/commands/hooks-commands.js +56 -3
- package/dist/cli/commands/primer-command.d.ts +59 -0
- package/dist/cli/commands/primer-command.js +160 -0
- package/dist/cli/commands/qa-commands.js +1 -2
- package/dist/cli/commands/sediment-commands.d.ts +1 -1
- package/dist/cli/commands/slice-commands.js +20 -8
- package/dist/cli/commands/statusline-commands.d.ts +1 -0
- package/dist/cli/commands/statusline-commands.js +17 -3
- package/dist/cli/commands/tech-commands.d.ts +1 -1
- package/dist/cli/program.js +8 -0
- package/dist/services/artifacts/request-artifact-service.d.ts +2 -2
- package/dist/services/artifacts/request-artifact-service.js +10 -1
- package/dist/services/audit/enforcers/lint-reference-shape.js +18 -4
- package/dist/services/code/auto-compact-orchestrator.js +9 -1
- package/dist/services/code/batch-heartbeat-poller.js +26 -0
- package/dist/services/code/orchestrator-can-do.d.ts +24 -0
- package/dist/services/code/orchestrator-can-do.js +48 -2
- package/dist/services/code/post-compact-detector.d.ts +1 -1
- package/dist/services/compact-statusline/compact-statusline-service.js +19 -11
- package/dist/services/config/config-safety.d.ts +23 -0
- package/dist/services/config/config-safety.js +62 -0
- package/dist/services/config/config-service.d.ts +1 -1
- package/dist/services/config/config-service.js +2 -4
- package/dist/services/container/container-lease.js +21 -8
- package/dist/services/context/spillover-store.d.ts +1 -1
- package/dist/services/crystallization/crystallization-types.js +20 -8
- package/dist/services/crystallization/evidence-brief-builder.js +22 -11
- package/dist/services/dispatch/dispatch-record-writer.js +231 -141
- package/dist/services/evolution/evolution-types.js +38 -21
- package/dist/services/feedback/feedback-promotion-service.js +9 -1
- package/dist/services/hooks/pre-tool-code-gate.d.ts +46 -0
- package/dist/services/hooks/pre-tool-code-gate.js +91 -0
- package/dist/services/ide/current-model-detector.js +1 -2
- package/dist/services/lint/detect-eslint.js +8 -5
- package/dist/services/lint/eslint-runner.d.ts +1 -2
- package/dist/services/lint/eslint-runner.js +86 -23
- package/dist/services/lint/npx-resolver.d.ts +6 -0
- package/dist/services/lint/npx-resolver.js +47 -0
- package/dist/services/loop/loop-release-types.js +32 -13
- package/dist/services/loop/spec-service.d.ts +7 -1
- package/dist/services/loop/spec-service.js +218 -182
- package/dist/services/rd/rd-service.js +22 -9
- package/dist/services/scan/archetype-service.js +21 -9
- package/dist/services/session/binding-store.js +1 -2
- package/dist/services/share/bundle-reader.d.ts +6 -9
- package/dist/services/share/bundle-reader.js +297 -223
- package/dist/services/skills/hooks-settings-service.d.ts +33 -0
- package/dist/services/skills/hooks-settings-service.js +48 -3
- package/dist/services/skills/{outer-cache-hook-constants.d.ts → session-start-hook-constants.d.ts} +20 -0
- package/dist/services/skills/{outer-cache-hook-constants.js → session-start-hook-constants.js} +20 -0
- package/dist/services/skills/skill-statusline-renderer.d.ts +50 -1
- package/dist/services/skills/skill-statusline-renderer.js +96 -8
- package/dist/services/skills/skill-statusline-service.d.ts +41 -0
- package/dist/services/skills/skill-statusline-service.js +56 -6
- package/dist/services/slice/calibration-store.js +21 -8
- package/dist/services/slice/slice-check-service.js +35 -14
- package/dist/services/slice/slice-decompose-service.js +459 -223
- package/dist/services/standards/project-context.js +135 -73
- package/dist/services/verdict/envelopes.d.ts +1 -2
- package/dist/services/verdict/envelopes.js +1 -2
- package/dist/services/verdict/verdict-aggregator.d.ts +1 -2
- package/dist/services/vm/vm-lease.js +19 -8
- package/dist/services/worktree/worktree-lease.d.ts +0 -10
- package/dist/services/worktree/worktree-lease.js +19 -8
- package/dist/shared/fs-utils.d.ts +1 -1
- package/package.json +7 -4
- 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
|
|
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
|
-
|
|
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: "
|
|
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: '
|
|
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 ===
|
|
46
|
+
if (value === ESLINT_SEVERITY_ERROR)
|
|
31
47
|
return 'error';
|
|
32
|
-
if (value ===
|
|
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:
|
|
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,
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
];
|
|
203
|
-
|
|
204
|
-
|
|
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:
|
|
261
|
+
cwd: projectRoot,
|
|
220
262
|
encoding: 'utf8',
|
|
221
|
-
timeout: options.timeoutMs ??
|
|
222
|
-
maxBuffer:
|
|
263
|
+
timeout: options.timeoutMs ?? ESLINT_DEFAULT_TIMEOUT_MS,
|
|
264
|
+
maxBuffer: OUTPUT_BUFFER_BYTES
|
|
223
265
|
};
|
|
224
|
-
|
|
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,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(
|
|
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(
|
|
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(
|
|
100
|
-
scenario: z.string().trim().min(1).max(
|
|
101
|
-
trigger_policy: z.string().trim().min(1).max(
|
|
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(
|
|
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(
|
|
110
|
-
feedback_policy: z.string().trim().min(1).max(
|
|
111
|
-
evolution_policy: z.string().trim().min(1).max(
|
|
112
|
-
evaluator_policy: z.array(z.string().trim().min(1).max(
|
|
113
|
-
linked_bees: z.array(z.string().min(1).max(
|
|
114
|
-
run_history: z.array(z.string().min(1).max(
|
|
115
|
-
crystallization_evidence: z.array(z.string().min(1).max(
|
|
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;
|