peaks-loop 4.0.7 → 4.0.9
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 +58 -288
- package/dist/cli/commands/_register.js +2 -0
- package/dist/cli/commands/baseline-commands.d.ts +3 -0
- package/dist/cli/commands/baseline-commands.js +146 -0
- package/dist/cli/commands/code-job-shape-commands.js +8 -11
- package/dist/cli/commands/code-mode-gate-commands.js +1 -43
- package/dist/cli/commands/code-runtime-commands.js +36 -14
- package/dist/cli/commands/core/skill-command.js +75 -13
- package/dist/cli/commands/core/standards-command.js +5 -50
- package/dist/cli/commands/dispatch-commands.d.ts +20 -0
- package/dist/cli/commands/dispatch-commands.js +39 -38
- package/dist/cli/commands/dispatch-from-dag.js +17 -0
- package/dist/cli/commands/heartbeat-commands.d.ts +21 -0
- package/dist/cli/commands/heartbeat-commands.js +40 -0
- package/dist/cli/commands/hook-handle.js +5 -2
- package/dist/cli/commands/hooks-commands.js +9 -26
- package/dist/cli/commands/loop-eval-commands.js +5 -2
- package/dist/cli/commands/statusline-commands.js +1 -1
- package/dist/cli/commands/sub-agent-commands.js +0 -2
- package/dist/cli/commands/sub-agent-shared.d.ts +7 -9
- package/dist/cli/commands/workflow-commands.js +8 -0
- package/dist/cli/commands/workflow-lifecycle-commands.d.ts +63 -0
- package/dist/cli/commands/workflow-lifecycle-commands.js +302 -0
- package/dist/cli/commands/workspace/init-command.js +19 -0
- package/dist/services/audit/backing-detector.d.ts +2 -19
- package/dist/services/audit/backing-detector.js +7 -66
- package/dist/services/audit/enforcers/active-skill-resolver.d.ts +25 -9
- package/dist/services/audit/enforcers/active-skill-resolver.js +83 -21
- package/dist/services/audit/enforcers/lint-audit-regression.js +1 -10
- package/dist/services/capability-audit-service/cross-check.d.ts +8 -0
- package/dist/services/capability-audit-service/cross-check.js +11 -0
- package/dist/services/capability-audit-service/index.d.ts +3 -0
- package/dist/services/capability-audit-service/index.js +2 -0
- package/dist/services/capability-audit-service/runner.d.ts +27 -0
- package/dist/services/capability-audit-service/runner.js +40 -0
- package/dist/services/capability-audit-service/staleness.d.ts +1 -0
- package/dist/services/capability-audit-service/staleness.js +7 -0
- package/dist/services/capability-audit-service/types.d.ts +24 -0
- package/dist/services/capability-audit-service/types.js +1 -0
- package/dist/services/capability-baseline/store.d.ts +33 -0
- package/dist/services/capability-baseline/store.js +80 -0
- package/dist/services/capability-baseline/types.d.ts +45 -0
- package/dist/services/capability-baseline/types.js +5 -0
- package/dist/services/capability-baseline/validator.d.ts +19 -0
- package/dist/services/capability-baseline/validator.js +38 -0
- package/dist/services/capability-guard-runner/contracts/J01.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J01.js +48 -0
- package/dist/services/capability-guard-runner/contracts/J02.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J02.js +34 -0
- package/dist/services/capability-guard-runner/contracts/J03.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J03.js +24 -0
- package/dist/services/capability-guard-runner/contracts/J04.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J04.js +40 -0
- package/dist/services/capability-guard-runner/contracts/J05.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J05.js +21 -0
- package/dist/services/capability-guard-runner/contracts/J06.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J06.js +43 -0
- package/dist/services/capability-guard-runner/contracts/J07.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J07.js +53 -0
- package/dist/services/capability-guard-runner/contracts/J08.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J08.js +43 -0
- package/dist/services/capability-guard-runner/contracts/J09.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J09.js +44 -0
- package/dist/services/capability-guard-runner/contracts/J10.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J10.js +40 -0
- package/dist/services/capability-guard-runner/contracts/J11.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J11.js +39 -0
- package/dist/services/capability-guard-runner/contracts/J12.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J12.js +34 -0
- package/dist/services/capability-guard-runner/contracts/J13.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J13.js +45 -0
- package/dist/services/capability-guard-runner/contracts/J14.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J14.js +36 -0
- package/dist/services/capability-guard-runner/contracts/J15.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J15.js +40 -0
- package/dist/services/capability-guard-runner/diff.d.ts +2 -0
- package/dist/services/capability-guard-runner/diff.js +7 -0
- package/dist/services/capability-guard-runner/runner.d.ts +9 -0
- package/dist/services/capability-guard-runner/runner.js +22 -0
- package/dist/services/capability-guard-runner/types.d.ts +39 -0
- package/dist/services/capability-guard-runner/types.js +1 -0
- package/dist/services/code/auto-compact-orchestrator.d.ts +35 -2
- package/dist/services/code/auto-compact-orchestrator.js +29 -9
- package/dist/services/context/build-dispatch-system-prompt.js +0 -18
- package/dist/services/dispatch/dispatch-record-writer.d.ts +25 -1
- package/dist/services/dispatch/dispatch-record-writer.js +85 -15
- package/dist/services/dispatch/test-tool-detection.d.ts +0 -14
- package/dist/services/dispatch/test-tool-detection.js +0 -22
- package/dist/services/doctor/doctor-service/checks/skill-presence.d.ts +8 -0
- package/dist/services/doctor/doctor-service/checks/skill-presence.js +9 -1
- package/dist/services/evolution/evolution-types.d.ts +16 -16
- package/dist/services/final-review/final-review-service.d.ts +8 -0
- package/dist/services/final-review/final-review-service.js +14 -0
- package/dist/services/final-review/final-review-types.d.ts +1 -1
- package/dist/services/hooks/presence-marker-detector.d.ts +6 -0
- package/dist/services/hooks/presence-marker-detector.js +48 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +20 -0
- package/dist/services/ide/adapters/codex-adapter.js +20 -1
- package/dist/services/ide/adapters/cursor-adapter.js +21 -1
- package/dist/services/ide/adapters/hermes-adapter.js +20 -1
- package/dist/services/ide/adapters/openclaw-adapter.js +20 -1
- package/dist/services/ide/adapters/qoder-adapter.js +20 -1
- package/dist/services/ide/adapters/tongyi-lingma-adapter.js +20 -1
- package/dist/services/ide/adapters/trae-adapter.js +21 -1
- package/dist/services/ide/adapters/zcode-adapter.js +19 -0
- package/dist/services/ide/ide-types.d.ts +15 -0
- package/dist/services/job/job-progress-store.d.ts +2 -2
- package/dist/services/job/job-types.d.ts +26 -26
- package/dist/services/observability/observability-service.d.ts +4 -4
- package/dist/services/scan/archetype-service.js +2 -52
- package/dist/services/sediment/json-schema.d.ts +4 -4
- package/dist/services/session/caller-binding-service.d.ts +21 -0
- package/dist/services/session/caller-binding-service.js +35 -0
- package/dist/services/session/caller-id-types.d.ts +62 -13
- package/dist/services/session/caller-id-types.js +25 -13
- package/dist/services/session/index.d.ts +4 -2
- package/dist/services/session/index.js +8 -2
- package/dist/services/session/platform-fallbacks.d.ts +25 -17
- package/dist/services/session/platform-fallbacks.js +26 -28
- package/dist/services/session/resolve-caller-id.d.ts +64 -29
- package/dist/services/session/resolve-caller-id.js +122 -48
- package/dist/services/session/session-manager.d.ts +0 -29
- package/dist/services/session/session-manager.js +0 -33
- package/dist/services/session/session-resume-service.d.ts +4 -0
- package/dist/services/session/session-resume-service.js +4 -0
- package/dist/services/skills/hooks-settings-service.d.ts +0 -41
- package/dist/services/skills/hooks-settings-service.js +5 -61
- package/dist/services/skills/presence-lease-service.d.ts +73 -0
- package/dist/services/skills/presence-lease-service.js +358 -0
- package/dist/services/skills/presence-lease-types.d.ts +75 -0
- package/dist/services/skills/presence-lease-types.js +16 -0
- package/dist/services/skills/skill-presence-service.js +86 -22
- package/dist/services/skills/skill-statusline-renderer.d.ts +46 -56
- package/dist/services/skills/skill-statusline-renderer.js +312 -100
- package/dist/services/slice/slice-check-service.js +0 -59
- package/dist/services/standards/project-standards-service.js +0 -44
- package/dist/services/workflow/workflow-graph-store.d.ts +77 -0
- package/dist/services/workflow/workflow-graph-store.js +278 -0
- package/dist/services/workflow/workflow-graph-types.d.ts +56 -0
- package/dist/services/workflow/workflow-graph-types.js +67 -0
- package/dist/services/workflow/workflow-inflight-probe.d.ts +52 -0
- package/dist/services/workflow/workflow-inflight-probe.js +86 -0
- package/dist/services/workflow/workflow-node-lifecycle.d.ts +67 -0
- package/dist/services/workflow/workflow-node-lifecycle.js +348 -0
- package/dist/services/workflow/workflow-presence-lifecycle.d.ts +58 -0
- package/dist/services/workflow/workflow-presence-lifecycle.js +196 -0
- package/dist/services/workspace/reconcile-service.d.ts +31 -0
- package/dist/services/workspace/reconcile-service.js +132 -1
- package/package.json +5 -4
- package/skills/peaks-code/SKILL.md +5 -43
- package/skills/peaks-code/references/completion-handoff.md +1 -1
- package/skills/peaks-code/references/skill-presence-and-title.md +1 -1
- package/dist/cli/commands/sub-agent-exec-command.d.ts +0 -25
- package/dist/cli/commands/sub-agent-exec-command.js +0 -132
- package/dist/services/standards/find-skills-integration.d.ts +0 -59
- package/dist/services/standards/find-skills-integration.js +0 -225
|
@@ -4,26 +4,42 @@
|
|
|
4
4
|
* Resolves the active peak skill name for the current session, so hook
|
|
5
5
|
* enforcers (e.g. code-ban) can decide whether to fire.
|
|
6
6
|
*
|
|
7
|
+
* Slice 4.0.8 (D1 + D4a): prefer the canonical
|
|
8
|
+
* `readPresenceLease` projection from `presence-lease-service.ts`.
|
|
9
|
+
* The 4.0.7 legacy `active-skill-<callerId>.json` walk is preserved
|
|
10
|
+
* as a 1-minor-release back-compat fallback, gated on the
|
|
11
|
+
* `legacyPresence: true` opt-in. By default the canonical projection
|
|
12
|
+
* is the only source of truth — the read returns `{ skill, callerId,
|
|
13
|
+
* sessionId, source: 'canonical' | 'legacy' | 'none' }`.
|
|
14
|
+
*
|
|
7
15
|
* Per `src/services/session/caller-id-types.ts`: the active-skill file is
|
|
8
|
-
* at `.peaks/_runtime/<peakSessionId>/active-skill-<callerId>.json
|
|
16
|
+
* at `.peaks/_runtime/<peakSessionId>/active-skill-<callerId>.json`
|
|
17
|
+
* (legacy) or `.peaks/_runtime/<sid>/presence-index/<callerId>.json`
|
|
18
|
+
* (canonical index → lease file).
|
|
9
19
|
*
|
|
10
20
|
* Resolution order (graceful degradation — never throws):
|
|
11
21
|
* 1. PEAKS_ACTIVE_SKILL env var (explicit override, used by tests)
|
|
12
|
-
* 2.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
22
|
+
* 2. Canonical lease projection (presence-lease-service.readPresenceLease)
|
|
23
|
+
* 3. Legacy `.peaks/_runtime/<sid>/active-skill-*.json` walk, when
|
|
24
|
+
* `legacyPresence === true` is passed by the caller
|
|
25
|
+
* 4. null (caller did not set a skill; enforcers can decide to skip)
|
|
15
26
|
*/
|
|
16
27
|
export interface ActiveSkillResolution {
|
|
17
28
|
readonly skill: string | null;
|
|
18
29
|
readonly callerId: string | null;
|
|
19
30
|
readonly sessionId: string | null;
|
|
20
|
-
|
|
31
|
+
/** `canonical` = presence-lease-service; `legacy` = per-caller active-skill file; `env` = test override; `none` = nothing wired. */
|
|
32
|
+
readonly source: 'env' | 'file' | 'canonical' | 'none';
|
|
21
33
|
}
|
|
22
34
|
/**
|
|
23
35
|
* Resolve the active peak skill for the current hook invocation.
|
|
24
36
|
*
|
|
25
|
-
* Reads PEAKS_ACTIVE_SKILL first (test override), then
|
|
26
|
-
*
|
|
27
|
-
* the
|
|
37
|
+
* Reads PEAKS_ACTIVE_SKILL first (test override), then the canonical
|
|
38
|
+
* lease projection (RD §4 / §6 — the lease + index is the source of
|
|
39
|
+
* truth in 4.0.8), then — only when the caller explicitly opts in via
|
|
40
|
+
* `legacyPresence: true` — the legacy `.peaks/_runtime/<sid>/active-skill-*.json`
|
|
41
|
+
* walk. Returns the first match.
|
|
28
42
|
*/
|
|
29
|
-
export declare function resolveActiveSkillForCaller(projectRoot: string
|
|
43
|
+
export declare function resolveActiveSkillForCaller(projectRoot: string, opts?: {
|
|
44
|
+
legacyPresence?: boolean;
|
|
45
|
+
}): ActiveSkillResolution;
|
|
@@ -4,28 +4,43 @@
|
|
|
4
4
|
* Resolves the active peak skill name for the current session, so hook
|
|
5
5
|
* enforcers (e.g. code-ban) can decide whether to fire.
|
|
6
6
|
*
|
|
7
|
+
* Slice 4.0.8 (D1 + D4a): prefer the canonical
|
|
8
|
+
* `readPresenceLease` projection from `presence-lease-service.ts`.
|
|
9
|
+
* The 4.0.7 legacy `active-skill-<callerId>.json` walk is preserved
|
|
10
|
+
* as a 1-minor-release back-compat fallback, gated on the
|
|
11
|
+
* `legacyPresence: true` opt-in. By default the canonical projection
|
|
12
|
+
* is the only source of truth — the read returns `{ skill, callerId,
|
|
13
|
+
* sessionId, source: 'canonical' | 'legacy' | 'none' }`.
|
|
14
|
+
*
|
|
7
15
|
* Per `src/services/session/caller-id-types.ts`: the active-skill file is
|
|
8
|
-
* at `.peaks/_runtime/<peakSessionId>/active-skill-<callerId>.json
|
|
16
|
+
* at `.peaks/_runtime/<peakSessionId>/active-skill-<callerId>.json`
|
|
17
|
+
* (legacy) or `.peaks/_runtime/<sid>/presence-index/<callerId>.json`
|
|
18
|
+
* (canonical index → lease file).
|
|
9
19
|
*
|
|
10
20
|
* Resolution order (graceful degradation — never throws):
|
|
11
21
|
* 1. PEAKS_ACTIVE_SKILL env var (explicit override, used by tests)
|
|
12
|
-
* 2.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
22
|
+
* 2. Canonical lease projection (presence-lease-service.readPresenceLease)
|
|
23
|
+
* 3. Legacy `.peaks/_runtime/<sid>/active-skill-*.json` walk, when
|
|
24
|
+
* `legacyPresence === true` is passed by the caller
|
|
25
|
+
* 4. null (caller did not set a skill; enforcers can decide to skip)
|
|
15
26
|
*/
|
|
16
27
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
17
28
|
import { join } from 'node:path';
|
|
18
29
|
import { getSessionIdCanonical } from '../../session/session-manager.js';
|
|
19
30
|
import { getSessionDir } from '../../session/getSessionDir.js';
|
|
31
|
+
import { readPresenceLease } from '../../skills/presence-lease-service.js';
|
|
32
|
+
import { getCurrentSessionId } from '../../skills/skill-presence-service.js';
|
|
20
33
|
const ACTIVE_SKILL_PREFIX = 'active-skill-';
|
|
21
34
|
/**
|
|
22
35
|
* Resolve the active peak skill for the current hook invocation.
|
|
23
36
|
*
|
|
24
|
-
* Reads PEAKS_ACTIVE_SKILL first (test override), then
|
|
25
|
-
*
|
|
26
|
-
* the
|
|
37
|
+
* Reads PEAKS_ACTIVE_SKILL first (test override), then the canonical
|
|
38
|
+
* lease projection (RD §4 / §6 — the lease + index is the source of
|
|
39
|
+
* truth in 4.0.8), then — only when the caller explicitly opts in via
|
|
40
|
+
* `legacyPresence: true` — the legacy `.peaks/_runtime/<sid>/active-skill-*.json`
|
|
41
|
+
* walk. Returns the first match.
|
|
27
42
|
*/
|
|
28
|
-
export function resolveActiveSkillForCaller(projectRoot) {
|
|
43
|
+
export function resolveActiveSkillForCaller(projectRoot, opts) {
|
|
29
44
|
const envOverride = process.env.PEAKS_ACTIVE_SKILL;
|
|
30
45
|
if (typeof envOverride === 'string' && envOverride.length > 0) {
|
|
31
46
|
return { skill: envOverride, callerId: null, sessionId: null, source: 'env' };
|
|
@@ -40,32 +55,79 @@ export function resolveActiveSkillForCaller(projectRoot) {
|
|
|
40
55
|
if (sessionId === null) {
|
|
41
56
|
return { skill: null, callerId: null, sessionId: null, source: 'none' };
|
|
42
57
|
}
|
|
58
|
+
// 4.0.8 preferred: read the canonical lease projection. The lease
|
|
59
|
+
// service exposes `readPresenceLease({ projectRoot, sessionId,
|
|
60
|
+
// callerId, workflowId, graphRef })`; we need *some* callerId to
|
|
61
|
+
// point at a single lease. Hook enforcers don't have a callerId
|
|
62
|
+
// (they're per-IDE, per-window), so we walk the canonical
|
|
63
|
+
// presence-index to find any active lease. When the index is empty
|
|
64
|
+
// (e.g. ad-hoc / pre-migration projects) we fall through to the
|
|
65
|
+
// legacy walk below.
|
|
43
66
|
const sessionDir = getSessionDir(projectRoot, sessionId);
|
|
44
67
|
if (!existsSync(sessionDir)) {
|
|
45
|
-
return { skill: null, callerId: null, sessionId
|
|
68
|
+
return { skill: null, callerId: null, sessionId, source: 'none' };
|
|
46
69
|
}
|
|
47
|
-
let entries;
|
|
70
|
+
let entries = [];
|
|
48
71
|
try {
|
|
49
72
|
entries = readdirSync(sessionDir);
|
|
50
73
|
}
|
|
51
74
|
catch {
|
|
52
|
-
|
|
75
|
+
entries = [];
|
|
53
76
|
}
|
|
54
|
-
|
|
55
|
-
|
|
77
|
+
const indexEntries = entries.filter((e) => !e.startsWith(ACTIVE_SKILL_PREFIX) && e.endsWith('.json') && !e.includes('.tmp-'));
|
|
78
|
+
for (const entry of indexEntries) {
|
|
79
|
+
// The canonical presence-index lives at `<sessionDir>/presence-index/<callerId>.json`.
|
|
80
|
+
// We synthesise the read from any presence-* lease file we find;
|
|
81
|
+
// the readPresenceLease call surfaces a typed `PresenceProjection`
|
|
82
|
+
// and we map `lease.skill` → resolution.
|
|
83
|
+
if (!entry.startsWith('presence-'))
|
|
84
|
+
continue;
|
|
85
|
+
const callerId = entry.slice('presence-'.length, -'.json'.length);
|
|
86
|
+
// Derive workflowId from the same file name (canonical
|
|
87
|
+
// leaseFilePath encodes `presence-<caller>-<workflow>.json`).
|
|
88
|
+
const lastDash = callerId.lastIndexOf('-');
|
|
89
|
+
if (lastDash < 0)
|
|
56
90
|
continue;
|
|
57
|
-
const
|
|
58
|
-
const
|
|
91
|
+
const workflowId = callerId.slice(lastDash + 1);
|
|
92
|
+
const pureCaller = callerId.slice(0, lastDash);
|
|
59
93
|
try {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
94
|
+
const projection = readPresenceLease({
|
|
95
|
+
projectRoot,
|
|
96
|
+
sessionId,
|
|
97
|
+
callerId: pureCaller,
|
|
98
|
+
workflowId,
|
|
99
|
+
graphRef: `graphs/${workflowId}.json`,
|
|
100
|
+
});
|
|
101
|
+
if (projection.lease !== null && typeof projection.lease.skill === 'string' && projection.lease.skill.length > 0) {
|
|
102
|
+
return { skill: projection.lease.skill, callerId: pureCaller, sessionId, source: 'canonical' };
|
|
64
103
|
}
|
|
65
104
|
}
|
|
66
|
-
catch {
|
|
67
|
-
|
|
105
|
+
catch { /* fall through to legacy walk */ }
|
|
106
|
+
}
|
|
107
|
+
// Legacy fall-back (one minor release). Gated on `legacyPresence:
|
|
108
|
+
// true` so production hooks prefer the canonical projection.
|
|
109
|
+
if (opts?.legacyPresence === true) {
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
if (!entry.startsWith(ACTIVE_SKILL_PREFIX) || !entry.endsWith('.json'))
|
|
112
|
+
continue;
|
|
113
|
+
const callerId = entry.slice(ACTIVE_SKILL_PREFIX.length, -'.json'.length);
|
|
114
|
+
const filePath = join(sessionDir, entry);
|
|
115
|
+
try {
|
|
116
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
117
|
+
const parsed = JSON.parse(raw);
|
|
118
|
+
if (typeof parsed.skill === 'string' && parsed.skill.length > 0) {
|
|
119
|
+
return { skill: parsed.skill, callerId, sessionId, source: 'file' };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
123
|
+
// skip malformed file
|
|
124
|
+
}
|
|
68
125
|
}
|
|
69
126
|
}
|
|
127
|
+
// Suppress unused-import warnings for symbols reserved for the
|
|
128
|
+
// migration window. `getCurrentSessionId` is kept so the legacy
|
|
129
|
+
// walk can still resolve a session id if the canonical resolver
|
|
130
|
+
// returns null (e.g. ad-hoc projects without a `.peaks/_runtime/session.json`).
|
|
131
|
+
void getCurrentSessionId;
|
|
70
132
|
return { skill: null, callerId: null, sessionId, source: 'none' };
|
|
71
133
|
}
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
import { existsSync, readFileSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { RED_LINE_CATALOG } from '../red-line-catalog.js';
|
|
18
|
-
import { resolvePeaksLoopRoot } from '../backing-detector.js';
|
|
19
18
|
export const CATALOG_STABILITY_GROWTH_CAP = 0.20;
|
|
20
19
|
export const CATALOG_STABILITY_WINDOW_DAYS = 90;
|
|
21
20
|
export const RUNTIME_BUDGET_MS = 2000;
|
|
@@ -42,19 +41,11 @@ export function lintCatalogStability(input) {
|
|
|
42
41
|
return [syntheticHit('rl-audit-catalog-stability-001', 'catalog size has not grown > 20% in the last 90 days', `(growth ${(growth * 100).toFixed(1)}% over 90 days; currentSize=${input.currentSize}, priorSize=${input.sizeNinetyDaysAgo})`)];
|
|
43
42
|
}
|
|
44
43
|
export function lintNoOrphanEnforcer(projectRoot) {
|
|
45
|
-
// Slice 4.0.7-dogfood-PR-1: catalog enforcerRef paths are written
|
|
46
|
-
// relative to the peaks-loop source root (catalog + enforcers both
|
|
47
|
-
// live at <root>/src/services/audit/...). Resolving against the
|
|
48
|
-
// audited --project root produces false orphans on every downstream
|
|
49
|
-
// project. Use resolvePeaksLoopRoot() and keep `projectRoot` for
|
|
50
|
-
// test/legacy callers (the parameter is intentionally ignored here).
|
|
51
|
-
void projectRoot;
|
|
52
|
-
const peaksLoopRoot = resolvePeaksLoopRoot();
|
|
53
44
|
const hits = [];
|
|
54
45
|
for (const entry of RED_LINE_CATALOG) {
|
|
55
46
|
if (!entry.enforcerRef)
|
|
56
47
|
continue;
|
|
57
|
-
const absPath = join(
|
|
48
|
+
const absPath = join(projectRoot, entry.enforcerRef);
|
|
58
49
|
if (!existsSync(absPath)) {
|
|
59
50
|
hits.push(syntheticHit('rl-audit-no-orphan-enforcer-001', 'every enforcerRef points to a real file', `(enforcerRef "${entry.enforcerRef}" for ${entry.id} does not exist on disk)`));
|
|
60
51
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CrossCheck } from './types.js';
|
|
2
|
+
export declare function crossCheck(input: {
|
|
3
|
+
readonly guardPass: number;
|
|
4
|
+
readonly guardFail: number;
|
|
5
|
+
readonly independentPass: number;
|
|
6
|
+
readonly independentFail: number;
|
|
7
|
+
readonly karpathy: 'pass' | 'warn' | 'fail' | 'skipped';
|
|
8
|
+
}): CrossCheck;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function crossCheck(input) {
|
|
2
|
+
const guardVerdict = input.guardFail === 0 ? 'pass' : 'fail';
|
|
3
|
+
const indepVerdict = input.independentFail === 0 ? 'pass' : 'fail';
|
|
4
|
+
const guardVsAudit = guardVerdict === indepVerdict ? 'agree'
|
|
5
|
+
: (Math.abs(input.guardPass - input.independentPass) <= 1) ? 'partial'
|
|
6
|
+
: 'diverge';
|
|
7
|
+
const karpathyVsAudit = input.karpathy === 'skipped' ? 'partial'
|
|
8
|
+
: (input.karpathy === 'warn' ? 'partial'
|
|
9
|
+
: (input.karpathy === 'pass' ? guardVsAudit : 'diverge'));
|
|
10
|
+
return { guardVsAudit, karpathyVsAudit };
|
|
11
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CapabilityAuditResult } from './types.js';
|
|
2
|
+
import type { JourneyId } from '../capability-baseline/types.js';
|
|
3
|
+
import type { GuardRunResult } from '../capability-guard-runner/types.js';
|
|
4
|
+
export interface RunAuditInput {
|
|
5
|
+
readonly projectRoot: string;
|
|
6
|
+
readonly sessionId: string;
|
|
7
|
+
readonly journeyId: JourneyId;
|
|
8
|
+
readonly llmRunner: {
|
|
9
|
+
call(system: string, user: string, opts: {
|
|
10
|
+
maxTokens: number;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
output: string;
|
|
13
|
+
tokens: {
|
|
14
|
+
input: number;
|
|
15
|
+
output: number;
|
|
16
|
+
};
|
|
17
|
+
}>;
|
|
18
|
+
};
|
|
19
|
+
readonly guardSummary: {
|
|
20
|
+
readonly pass: number;
|
|
21
|
+
readonly fail: number;
|
|
22
|
+
readonly skipped: number;
|
|
23
|
+
readonly total: number;
|
|
24
|
+
readonly results: ReadonlyArray<GuardRunResult>;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export declare function runAudit(input: RunAuditInput): Promise<CapabilityAuditResult>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { crossCheck } from './cross-check.js';
|
|
4
|
+
const SYSTEM = 'You are an INDEPENDENT audit scorer. Compare the supplied capability baseline to the supplied current behavior summary. Output a single JSON object: {"verdict":"consistent" | "drifted" | "inconclusive"}. No prose.';
|
|
5
|
+
export async function runAudit(input) {
|
|
6
|
+
const userPayload = JSON.stringify({ baselineJourneyId: input.journeyId, guard: input.guardSummary });
|
|
7
|
+
const r = await input.llmRunner.call(SYSTEM, userPayload, { maxTokens: 200 });
|
|
8
|
+
const { verdict: independentVerdict } = JSON.parse(r.output);
|
|
9
|
+
const xc = crossCheck({
|
|
10
|
+
guardPass: input.guardSummary.pass,
|
|
11
|
+
guardFail: input.guardSummary.fail,
|
|
12
|
+
independentPass: independentVerdict === 'consistent' ? 1 : 0,
|
|
13
|
+
independentFail: independentVerdict === 'drifted' ? 1 : 0,
|
|
14
|
+
karpathy: 'skipped'
|
|
15
|
+
});
|
|
16
|
+
let verdict = independentVerdict;
|
|
17
|
+
if (xc.guardVsAudit === 'diverge')
|
|
18
|
+
verdict = 'inconclusive';
|
|
19
|
+
const dimensions = [{
|
|
20
|
+
journeyId: input.journeyId,
|
|
21
|
+
consistencyScore: verdict === 'consistent' ? 1 : verdict === 'drifted' ? 0 : 0.5,
|
|
22
|
+
evidence: [
|
|
23
|
+
{ kind: 'guard-run', ref: `capability-guard-runner:${input.guardSummary.total}`, summary: `${input.guardSummary.pass} pass / ${input.guardSummary.fail} fail` },
|
|
24
|
+
{ kind: 'independent-eval', ref: 'audit-llm-context', summary: `independent verdict: ${independentVerdict}` }
|
|
25
|
+
]
|
|
26
|
+
}];
|
|
27
|
+
const auditId = `audit-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
|
28
|
+
const out = {
|
|
29
|
+
auditId,
|
|
30
|
+
auditedAt: new Date().toISOString(),
|
|
31
|
+
verdict,
|
|
32
|
+
dimensions,
|
|
33
|
+
crossCheck: xc,
|
|
34
|
+
requiresUserDecision: verdict === 'inconclusive'
|
|
35
|
+
};
|
|
36
|
+
const dir = join(input.projectRoot, '.peaks', '_runtime', input.sessionId, 'capability-audit');
|
|
37
|
+
mkdirSync(dir, { recursive: true });
|
|
38
|
+
writeFileSync(join(dir, `${auditId}.json`), JSON.stringify(out, null, 2));
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isStale(auditedAtIso: string, nowMs: number): boolean;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { JourneyId } from '../capability-baseline/types.js';
|
|
2
|
+
export type AuditVerdict = 'consistent' | 'drifted' | 'inconclusive';
|
|
3
|
+
export type AuditEvidenceKind = 'guard-run' | 'independent-eval' | 'karpathy-cross-check';
|
|
4
|
+
export interface AuditDimension {
|
|
5
|
+
readonly journeyId: JourneyId;
|
|
6
|
+
readonly consistencyScore: number;
|
|
7
|
+
readonly evidence: ReadonlyArray<{
|
|
8
|
+
readonly kind: AuditEvidenceKind;
|
|
9
|
+
readonly ref: string;
|
|
10
|
+
readonly summary: string;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
export interface CrossCheck {
|
|
14
|
+
readonly guardVsAudit: 'agree' | 'diverge' | 'partial';
|
|
15
|
+
readonly karpathyVsAudit: 'agree' | 'diverge' | 'partial';
|
|
16
|
+
}
|
|
17
|
+
export interface CapabilityAuditResult {
|
|
18
|
+
readonly auditId: string;
|
|
19
|
+
readonly auditedAt: string;
|
|
20
|
+
readonly verdict: AuditVerdict;
|
|
21
|
+
readonly dimensions: ReadonlyArray<AuditDimension>;
|
|
22
|
+
readonly crossCheck: CrossCheck;
|
|
23
|
+
readonly requiresUserDecision: boolean;
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { BaselineError, BaselineLock, CapabilityBaselineFile } from './types.js';
|
|
2
|
+
export declare function computeBaselineHash(file: CapabilityBaselineFile): string;
|
|
3
|
+
export declare function verifyLock(file: CapabilityBaselineFile, lock: BaselineLock): {
|
|
4
|
+
readonly ok: true;
|
|
5
|
+
} | {
|
|
6
|
+
readonly ok: false;
|
|
7
|
+
readonly error: BaselineError;
|
|
8
|
+
};
|
|
9
|
+
export declare function writeBaselineFile(input: {
|
|
10
|
+
readonly projectRoot: string;
|
|
11
|
+
readonly file: CapabilityBaselineFile;
|
|
12
|
+
}): {
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly lockPath: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function readBaselineFile(projectRoot: string): {
|
|
17
|
+
readonly ok: true;
|
|
18
|
+
readonly file: CapabilityBaselineFile;
|
|
19
|
+
readonly lock: BaselineLock;
|
|
20
|
+
readonly path: string;
|
|
21
|
+
readonly lockPath: string;
|
|
22
|
+
} | {
|
|
23
|
+
readonly ok: false;
|
|
24
|
+
readonly error: BaselineError;
|
|
25
|
+
};
|
|
26
|
+
export declare function historySnapshot(input: {
|
|
27
|
+
readonly projectRoot: string;
|
|
28
|
+
readonly version: string;
|
|
29
|
+
}): {
|
|
30
|
+
readonly path: string;
|
|
31
|
+
readonly lockPath: string;
|
|
32
|
+
};
|
|
33
|
+
export declare function currentPointer(sessionId: string, path: string, projectRoot: string): void;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/services/capability-baseline/store.ts
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
const CURRENT_DIR = (root) => join(root, 'openspec', 'baselines', 'current');
|
|
6
|
+
const FILE_PATH = (root) => join(CURRENT_DIR(root), 'capability-baseline.json');
|
|
7
|
+
const LOCK_PATH = (root) => join(CURRENT_DIR(root), 'capability-baseline.lock');
|
|
8
|
+
const POINTER = (root, sessionId) => join(root, '.peaks', '_runtime', sessionId, 'baselines', 'current.json');
|
|
9
|
+
function hashFor(file) {
|
|
10
|
+
const stripped = {
|
|
11
|
+
schemaVersion: file.schemaVersion,
|
|
12
|
+
version: file.version,
|
|
13
|
+
rows: file.rows
|
|
14
|
+
};
|
|
15
|
+
const json = JSON.stringify(stripped, Object.keys(stripped).sort());
|
|
16
|
+
return createHash('sha256').update(json).digest('hex');
|
|
17
|
+
}
|
|
18
|
+
export function computeBaselineHash(file) {
|
|
19
|
+
return hashFor(file);
|
|
20
|
+
}
|
|
21
|
+
export function verifyLock(file, lock) {
|
|
22
|
+
if (lock.signedBy !== 'SquabbyZ') {
|
|
23
|
+
return { ok: false, error: { code: 'BASELINE_NOT_SIGNED', message: 'lock is not signed by SquabbyZ' } };
|
|
24
|
+
}
|
|
25
|
+
if (hashFor(file) !== lock.baselineHash) {
|
|
26
|
+
return { ok: false, error: { code: 'BASELINE_HASH_MISMATCH', message: 'lock hash does not match file' } };
|
|
27
|
+
}
|
|
28
|
+
return { ok: true };
|
|
29
|
+
}
|
|
30
|
+
export function writeBaselineFile(input) {
|
|
31
|
+
const path = FILE_PATH(input.projectRoot);
|
|
32
|
+
const lockPath = LOCK_PATH(input.projectRoot);
|
|
33
|
+
mkdirSync(CURRENT_DIR(input.projectRoot), { recursive: true });
|
|
34
|
+
writeFileSync(path, JSON.stringify(input.file, null, 2));
|
|
35
|
+
const lock = {
|
|
36
|
+
baselineHash: hashFor(input.file),
|
|
37
|
+
signedBy: 'SquabbyZ',
|
|
38
|
+
signedAt: input.file.signedAt,
|
|
39
|
+
version: input.file.version
|
|
40
|
+
};
|
|
41
|
+
writeFileSync(lockPath, JSON.stringify(lock, null, 2));
|
|
42
|
+
return { path, lockPath };
|
|
43
|
+
}
|
|
44
|
+
export function readBaselineFile(projectRoot) {
|
|
45
|
+
const path = FILE_PATH(projectRoot);
|
|
46
|
+
const lockPath = LOCK_PATH(projectRoot);
|
|
47
|
+
if (!existsSync(path) || !existsSync(lockPath)) {
|
|
48
|
+
return { ok: false, error: { code: 'BASELINE_NOT_FOUND', message: `baseline missing at ${path}` } };
|
|
49
|
+
}
|
|
50
|
+
let file;
|
|
51
|
+
let lock;
|
|
52
|
+
try {
|
|
53
|
+
file = JSON.parse(readFileSync(path, 'utf8'));
|
|
54
|
+
lock = JSON.parse(readFileSync(lockPath, 'utf8'));
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
return { ok: false, error: { code: 'BASELINE_NOT_FOUND', message: e.message } };
|
|
58
|
+
}
|
|
59
|
+
if (file.signedBy !== 'SquabbyZ') {
|
|
60
|
+
return { ok: false, error: { code: 'BASELINE_NOT_SIGNED', message: 'baseline file is not signed by SquabbyZ' } };
|
|
61
|
+
}
|
|
62
|
+
const v = verifyLock(file, lock);
|
|
63
|
+
if (!v.ok)
|
|
64
|
+
return v;
|
|
65
|
+
return { ok: true, file, lock, path, lockPath };
|
|
66
|
+
}
|
|
67
|
+
export function historySnapshot(input) {
|
|
68
|
+
const target = join(input.projectRoot, 'openspec', 'baselines', 'history', input.version);
|
|
69
|
+
mkdirSync(target, { recursive: true });
|
|
70
|
+
const path = join(target, 'capability-baseline.json');
|
|
71
|
+
const lockPath = join(target, 'capability-baseline.lock');
|
|
72
|
+
copyFileSync(FILE_PATH(input.projectRoot), path);
|
|
73
|
+
copyFileSync(LOCK_PATH(input.projectRoot), lockPath);
|
|
74
|
+
return { path, lockPath };
|
|
75
|
+
}
|
|
76
|
+
export function currentPointer(sessionId, path, projectRoot) {
|
|
77
|
+
const p = POINTER(projectRoot, sessionId);
|
|
78
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
79
|
+
writeFileSync(p, JSON.stringify({ pointsTo: path, at: new Date().toISOString() }, null, 2));
|
|
80
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type JourneyId = 'J01' | 'J02' | 'J03' | 'J04' | 'J05' | 'J06' | 'J07' | 'J08' | 'J09' | 'J10' | 'J11' | 'J12' | 'J13' | 'J14' | 'J15';
|
|
2
|
+
export declare const P0_JOURNEY_IDS: ReadonlyArray<JourneyId>;
|
|
3
|
+
export interface InputCase {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly shape: string;
|
|
6
|
+
}
|
|
7
|
+
export interface OutputCase {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly shape: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ErrorCase {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly code: string;
|
|
14
|
+
}
|
|
15
|
+
export interface CapabilityBaselineRow {
|
|
16
|
+
readonly journeyId: JourneyId;
|
|
17
|
+
readonly intent: string;
|
|
18
|
+
readonly observable: {
|
|
19
|
+
readonly inputs: ReadonlyArray<InputCase>;
|
|
20
|
+
readonly outputs: ReadonlyArray<OutputCase>;
|
|
21
|
+
readonly errors: ReadonlyArray<ErrorCase>;
|
|
22
|
+
};
|
|
23
|
+
readonly invariants: ReadonlyArray<string>;
|
|
24
|
+
readonly forbiddenChanges: ReadonlyArray<string>;
|
|
25
|
+
readonly sourceFiles: ReadonlyArray<string>;
|
|
26
|
+
}
|
|
27
|
+
export interface CapabilityBaselineFile {
|
|
28
|
+
readonly schemaVersion: '2026-08-03';
|
|
29
|
+
readonly version: string;
|
|
30
|
+
readonly signedBy: 'SquabbyZ';
|
|
31
|
+
readonly signedAt: string;
|
|
32
|
+
readonly rows: ReadonlyArray<CapabilityBaselineRow>;
|
|
33
|
+
}
|
|
34
|
+
export interface BaselineLock {
|
|
35
|
+
readonly baselineHash: string;
|
|
36
|
+
readonly signedBy: 'SquabbyZ';
|
|
37
|
+
readonly signedAt: string;
|
|
38
|
+
readonly version: string;
|
|
39
|
+
}
|
|
40
|
+
export type BaselineErrorCode = 'BASELINE_NOT_FOUND' | 'BASELINE_HASH_MISMATCH' | 'BASELINE_NOT_SIGNED' | 'BASELINE_INCOMPLETE' | 'BASELINE_HISTORY_GAP' | 'BASELINE_ROW_SHAPE_INVALID' | 'BASELINE_FORBIDDEN_ALIAS';
|
|
41
|
+
export interface BaselineError {
|
|
42
|
+
readonly code: BaselineErrorCode;
|
|
43
|
+
readonly message: string;
|
|
44
|
+
readonly detail?: string;
|
|
45
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type BaselineError, type CapabilityBaselineFile, type CapabilityBaselineRow } from './types.js';
|
|
2
|
+
export declare function validateRowShape(row: CapabilityBaselineRow): {
|
|
3
|
+
readonly ok: true;
|
|
4
|
+
} | {
|
|
5
|
+
readonly ok: false;
|
|
6
|
+
readonly error: BaselineError;
|
|
7
|
+
};
|
|
8
|
+
export declare function validateBaselineFile(file: CapabilityBaselineFile): {
|
|
9
|
+
readonly ok: true;
|
|
10
|
+
} | {
|
|
11
|
+
readonly ok: false;
|
|
12
|
+
readonly error: BaselineError;
|
|
13
|
+
};
|
|
14
|
+
export declare function assertSignedBySquabbyZ(file: CapabilityBaselineFile): {
|
|
15
|
+
readonly ok: true;
|
|
16
|
+
} | {
|
|
17
|
+
readonly ok: false;
|
|
18
|
+
readonly error: BaselineError;
|
|
19
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/services/capability-baseline/validator.ts
|
|
2
|
+
import { P0_JOURNEY_IDS } from './types.js';
|
|
3
|
+
export function validateRowShape(row) {
|
|
4
|
+
if (row.intent.trim().length === 0)
|
|
5
|
+
return { ok: false, error: { code: 'BASELINE_ROW_SHAPE_INVALID', message: `row ${row.journeyId} has empty intent` } };
|
|
6
|
+
if (row.invariants.length === 0)
|
|
7
|
+
return { ok: false, error: { code: 'BASELINE_ROW_SHAPE_INVALID', message: `row ${row.journeyId} has no invariants` } };
|
|
8
|
+
if (row.forbiddenChanges.length === 0)
|
|
9
|
+
return { ok: false, error: { code: 'BASELINE_ROW_SHAPE_INVALID', message: `row ${row.journeyId} has no forbiddenChanges` } };
|
|
10
|
+
if (row.sourceFiles.length === 0)
|
|
11
|
+
return { ok: false, error: { code: 'BASELINE_ROW_SHAPE_INVALID', message: `row ${row.journeyId} has no sourceFiles` } };
|
|
12
|
+
return { ok: true };
|
|
13
|
+
}
|
|
14
|
+
export function validateBaselineFile(file) {
|
|
15
|
+
if (file.schemaVersion !== '2026-08-03') {
|
|
16
|
+
return { ok: false, error: { code: 'BASELINE_ROW_SHAPE_INVALID', message: `unsupported schemaVersion ${file.schemaVersion}` } };
|
|
17
|
+
}
|
|
18
|
+
const signed = assertSignedBySquabbyZ(file);
|
|
19
|
+
if (!signed.ok)
|
|
20
|
+
return signed;
|
|
21
|
+
for (const r of file.rows) {
|
|
22
|
+
const v = validateRowShape(r);
|
|
23
|
+
if (!v.ok)
|
|
24
|
+
return v;
|
|
25
|
+
}
|
|
26
|
+
const present = new Set(file.rows.map((r) => r.journeyId));
|
|
27
|
+
const missing = P0_JOURNEY_IDS.filter((j) => !present.has(j));
|
|
28
|
+
if (missing.length > 0) {
|
|
29
|
+
return { ok: false, error: { code: 'BASELINE_INCOMPLETE', message: `missing journeys: ${missing.join(',')}` } };
|
|
30
|
+
}
|
|
31
|
+
return { ok: true };
|
|
32
|
+
}
|
|
33
|
+
export function assertSignedBySquabbyZ(file) {
|
|
34
|
+
if (file.signedBy !== 'SquabbyZ') {
|
|
35
|
+
return { ok: false, error: { code: 'BASELINE_NOT_SIGNED', message: 'baseline not signed by SquabbyZ' } };
|
|
36
|
+
}
|
|
37
|
+
return { ok: true };
|
|
38
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const FIXTURES = [
|
|
4
|
+
['make', 'implement a CLI parser'],
|
|
5
|
+
['make', 'refactor the service'],
|
|
6
|
+
['make', 'write a blog article'],
|
|
7
|
+
['learn', 'author an SOP checklist'],
|
|
8
|
+
['check', 'run red-lines audit'],
|
|
9
|
+
['run', 'execute a workflow']
|
|
10
|
+
];
|
|
11
|
+
export async function runJ01Contract(ctx) {
|
|
12
|
+
const bin = process.env.PEAKS_BIN_OVERRIDE ?? join(ctx.projectRoot, 'bin', 'peaks.js');
|
|
13
|
+
let allOk = true;
|
|
14
|
+
let firstFailure = '';
|
|
15
|
+
for (const [command, input] of FIXTURES) {
|
|
16
|
+
try {
|
|
17
|
+
const stdout = execFileSync('node', [bin, command, input], {
|
|
18
|
+
cwd: ctx.projectRoot,
|
|
19
|
+
env: { ...process.env, PEAKS_CALLER_ID: `guard-J01-${ctx.sessionId}` }
|
|
20
|
+
}).toString('utf8');
|
|
21
|
+
const env = JSON.parse(stdout);
|
|
22
|
+
if (!env.ok) {
|
|
23
|
+
allOk = false;
|
|
24
|
+
firstFailure = `${command} ${input}`;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
allOk = false;
|
|
30
|
+
firstFailure = `${command} ${input}: ${e.message}`;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return allOk
|
|
35
|
+
? {
|
|
36
|
+
journeyId: 'J01',
|
|
37
|
+
contract: 'envelope-arg-shapes',
|
|
38
|
+
status: 'pass',
|
|
39
|
+
artifactPath: 'tests/integration/super-command-routing.test.ts'
|
|
40
|
+
}
|
|
41
|
+
: {
|
|
42
|
+
journeyId: 'J01',
|
|
43
|
+
contract: 'envelope-arg-shapes',
|
|
44
|
+
status: 'fail',
|
|
45
|
+
diff: { before: 'all 6 routing cases ok', after: firstFailure, reason: 'J01#1 broken: super-command routing NL path deviates from frozen baseline' },
|
|
46
|
+
artifactPath: 'tests/integration/super-command-routing.test.ts'
|
|
47
|
+
};
|
|
48
|
+
}
|