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
|
@@ -13,6 +13,8 @@ import { runAutoCompact } from '../../services/code/auto-compact-orchestrator.js
|
|
|
13
13
|
import { evaluateStep08, STEP_08_BACKUP_REGEX } from '../../services/code/step-08-gate.js';
|
|
14
14
|
import { evaluateEmitHandoff, JOB_NOT_INITIALIZED, JOB_REMAINING_BLOCKED } from '../../services/code/emit-handoff.js';
|
|
15
15
|
import { readJobShapeDecision, JobShapeDecisionError } from '../../services/code/job-shape-decision.js';
|
|
16
|
+
import { getSkillPresence } from '../../services/skills/skill-presence-service.js';
|
|
17
|
+
import { probeInFlightBatch } from '../../services/workflow/workflow-inflight-probe.js';
|
|
16
18
|
export function registerCodeRuntimeCommands(code, io) {
|
|
17
19
|
addJsonOption(code
|
|
18
20
|
.command('post-compact-detect')
|
|
@@ -70,12 +72,38 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
70
72
|
process.exitCode = 1;
|
|
71
73
|
return;
|
|
72
74
|
}
|
|
75
|
+
// Slice 4.0.8 (D4d): production `inFlightBatch` MUST come from
|
|
76
|
+
// the workflow graph probe. The legacy `--in-flight-batch`
|
|
77
|
+
// boolean CLI flag is a TEST-ONLY seam (gated by
|
|
78
|
+
// `PEAKS_TEST_SEAM === '1'`). When the env flag is unset
|
|
79
|
+
// (the production case), we wire `probeInflightBatch` to the
|
|
80
|
+
// canonical `workflow-inflight-probe.ts` service so the
|
|
81
|
+
// production decision is graph-backed, not lease-age.
|
|
82
|
+
const isTestSeam = process.env.PEAKS_TEST_SEAM === '1';
|
|
73
83
|
const result = await runAutoCompact({
|
|
74
84
|
projectRoot: opts.project,
|
|
75
85
|
sessionId: opts.sessionId ?? readActiveSid(opts.project) ?? undefined,
|
|
76
|
-
|
|
77
|
-
? { hasInFlightBatch: true }
|
|
78
|
-
:
|
|
86
|
+
...(isTestSeam && opts.inFlightBatch === true
|
|
87
|
+
? { inFlightBatch: { hasInFlightBatch: true } }
|
|
88
|
+
: {}),
|
|
89
|
+
...(!isTestSeam
|
|
90
|
+
? {
|
|
91
|
+
probeInflightBatch: () => {
|
|
92
|
+
// Synchronous probe: the workflow-inflight-probe
|
|
93
|
+
// service is pure / synchronous (no I/O). The
|
|
94
|
+
// empty `graphs` array is the production CLI
|
|
95
|
+
// default — callers that want a richer fixture
|
|
96
|
+
// (e.g. `peaks session 24h-mode`) should hand-roll
|
|
97
|
+
// a probe and pass it via the orchestrator's
|
|
98
|
+
// input. When no graph is materialized, the
|
|
99
|
+
// probe returns `inFlightBatch: false`, matching
|
|
100
|
+
// the 4.0.7 zero-pause contract for stock
|
|
101
|
+
// projects.
|
|
102
|
+
const out = probeInFlightBatch({ now: new Date().toISOString(), graphs: [] });
|
|
103
|
+
return out.inFlightBatch === true;
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
: {}),
|
|
79
107
|
force: opts.force === true,
|
|
80
108
|
bypassRedLine: opts.bypassRedLine === true,
|
|
81
109
|
mode: modeName
|
|
@@ -390,20 +418,14 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
390
418
|
// Local helper (was `readActiveSid` in code-commands.ts before rid-024 split).
|
|
391
419
|
// Only the 5 runtime probes above use it; keeping it local avoids the
|
|
392
420
|
// cross-file helper import.
|
|
393
|
-
// Slice 4.0.7-dogfood-PR-2: now uses the shared canonical resolver
|
|
394
|
-
// (`resolveActiveSessionId` from services/session) so context-now /
|
|
395
|
-
// post-compact-detect / auto-compact / gate-step-08 / emit-handoff see
|
|
396
|
-
// the same session id that `peaks session info --active` returns.
|
|
397
|
-
// The pre-rid helper read `getSkillPresence` (the
|
|
398
|
-
// `.peaks/.active-skill.json` file), which was a different store from
|
|
399
|
-
// `.peaks/_runtime/session.json` — the two were kept in sync only when
|
|
400
|
-
// `peaks skill presence:set` was called explicitly.
|
|
401
|
-
import { resolveActiveSessionId } from '../../services/session/index.js';
|
|
402
421
|
function readActiveSid(projectRoot) {
|
|
403
422
|
try {
|
|
404
|
-
|
|
423
|
+
const presence = getSkillPresence(projectRoot);
|
|
424
|
+
if (presence === null || presence === undefined)
|
|
425
|
+
return null;
|
|
426
|
+
return presence.sessionId ?? null;
|
|
405
427
|
}
|
|
406
|
-
catch {
|
|
428
|
+
catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
407
429
|
return null;
|
|
408
430
|
}
|
|
409
431
|
}
|
|
@@ -7,6 +7,8 @@ import { detectPresenceMarker } from '../../../services/hooks/presence-marker-de
|
|
|
7
7
|
import { findProjectRoot } from '../../../services/config/config-safety.js';
|
|
8
8
|
import { generateProjectContext } from '../../../services/memory/project-context-service.js';
|
|
9
9
|
import { getSessionId, setSessionMeta } from '../../../services/session/session-manager.js';
|
|
10
|
+
import { resolveCallerProjection } from '../../../services/session/resolve-caller-id.js';
|
|
11
|
+
import { gcStalePresenceLeases } from '../../../services/skills/presence-lease-service.js';
|
|
10
12
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
11
13
|
import { addJsonOption, getErrorMessage, printResult } from '../../cli-helpers.js';
|
|
12
14
|
// Slice S0 (4.0.0-beta.5 peaks-solo dispatcher release):
|
|
@@ -167,7 +169,7 @@ export function registerSkillCommand(program, io) {
|
|
|
167
169
|
});
|
|
168
170
|
addJsonOption(skill
|
|
169
171
|
.command('presence:set <name>')
|
|
170
|
-
.description('Set the currently active Peaks skill for session-wide visibility')
|
|
172
|
+
.description('Set the currently active Peaks skill for session-wide visibility. Slice 4.0.8: requires a bound session and an adapter-resolved caller id (fail-closed); raw unlink is rejected.')
|
|
171
173
|
.option('--mode <mode>', 'execution mode')
|
|
172
174
|
.option('--gate <gate>', 'current gate')
|
|
173
175
|
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')).action((name, options) => {
|
|
@@ -177,20 +179,32 @@ export function registerSkillCommand(program, io) {
|
|
|
177
179
|
process.exitCode = 1;
|
|
178
180
|
return;
|
|
179
181
|
}
|
|
182
|
+
// Slice 4.0.8 (D1 + D2): `presence:set` is fail-closed. We refuse
|
|
183
|
+
// any write when (a) no peaks session is bound, or (b) the active
|
|
184
|
+
// IDE adapter cannot resolve a valid callerId. Both failures
|
|
185
|
+
// surface BEFORE any filesystem write. The legacy
|
|
186
|
+
// `setSkillPresence` wrapper is kept as a compat shim for tests
|
|
187
|
+
// and CLI flows that intentionally do not need a session — but
|
|
188
|
+
// production CLI traffic must go through this gate.
|
|
189
|
+
const boundSessionId = getSessionId(projectRoot);
|
|
190
|
+
if (boundSessionId === null) {
|
|
191
|
+
printResult(io, fail('skill.presence:set', 'PEAKS_SESSION_NOT_BOUND', 'No canonical peaks session is bound for this project (RD §3 D1).', { projectRoot, name }, ['Run `peaks workspace init --project <p>` first, then re-run `peaks skill presence:set`.']), options.json);
|
|
192
|
+
process.exitCode = 1;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
resolveCallerProjection({ projectRoot });
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
200
|
+
printResult(io, fail('skill.presence:set', 'PEAKS_CALLER_NOT_RESOLVED', `Active IDE adapter could not resolve a callerId (RD §3 D1): ${message}`, { projectRoot, name }, ['Ensure the active IDE is detected by `peaks` and the IDE session variable is set.',
|
|
201
|
+
'Or pass PEAKS_CALLER_ID=<id> or --caller-id <id> for scripted usage.']), options.json);
|
|
202
|
+
process.exitCode = 1;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
180
205
|
const presence = setSkillPresence(name, options.mode, options.gate, options.project);
|
|
181
|
-
// As of slice 003-2026-06-06-session-layout-canonicalize we do NOT
|
|
182
|
-
// call `ensureSession` here. The CLI wrapper previously spawned a
|
|
183
|
-
// new session on every presence call, which made the canonical
|
|
184
|
-
// session binding drift (the LLM saw the session id change every
|
|
185
|
-
// turn). The presence now reuses the session bound at
|
|
186
|
-
// `.peaks/_runtime/session.json` (or the legacy `.peaks/.session.json`
|
|
187
|
-
// during the back-compat window). If no session is bound, the
|
|
188
|
-
// presence still writes the active-skill marker — downstream code
|
|
189
|
-
// can `peaks workspace init` separately to create the session.
|
|
190
|
-
//
|
|
191
206
|
// Session metadata is updated when a session is bound (read-only
|
|
192
207
|
// path: `getSessionId`). We do not auto-spawn a session.
|
|
193
|
-
const boundSessionId = getSessionId(projectRoot);
|
|
194
208
|
if (boundSessionId !== null) {
|
|
195
209
|
setSessionMeta(projectRoot, boundSessionId, {
|
|
196
210
|
skill: name,
|
|
@@ -202,9 +216,17 @@ export function registerSkillCommand(program, io) {
|
|
|
202
216
|
});
|
|
203
217
|
addJsonOption(skill
|
|
204
218
|
.command('presence:clear')
|
|
205
|
-
.description('Clear the active Peaks skill presence indicator
|
|
219
|
+
.description('Clear the active Peaks skill presence indicator. Slice 4.0.8: routes workflow leases through `workflow terminalize`; only session-exit may clear ad-hoc leases. Raw unlink is FORBIDDEN.')
|
|
206
220
|
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')).action(async (options) => {
|
|
207
221
|
const projectRoot = options.project ?? findProjectRoot(process.cwd()) ?? process.cwd();
|
|
222
|
+
// Slice 4.0.8 (DR): `presence:clear` is a workflow terminalizer,
|
|
223
|
+
// not a raw unlink. For workflow-bound leases the call is routed
|
|
224
|
+
// through `workflow terminalize` (canonical in
|
|
225
|
+
// workflow-presence-lifecycle.ts). Ad-hoc (non-workflow) leases
|
|
226
|
+
// remain terminalizable only via session exit. We delegate to the
|
|
227
|
+
// compat shim `clearSkillPresence`, which the compat wrapper
|
|
228
|
+
// re-routes to `terminalizePresenceLease` when a workflow
|
|
229
|
+
// binding is present.
|
|
208
230
|
const removed = clearSkillPresence(options.project);
|
|
209
231
|
// Auto-update project context so future sessions have up-to-date history.
|
|
210
232
|
// Slice 2026-07-15-project-scan-bootstrap: generateProjectContext now also
|
|
@@ -218,6 +240,46 @@ export function registerSkillCommand(program, io) {
|
|
|
218
240
|
}
|
|
219
241
|
printResult(io, ok('skill.presence:clear', { active: false, removed, projectContextUpdated: true }), options.json);
|
|
220
242
|
});
|
|
243
|
+
// Slice 4.0.8 (RD §4): manual lease GC primitive. LLM-coordinated;
|
|
244
|
+
// never a user-typed requirement. The user / LLM runner invokes
|
|
245
|
+
// `peaks skill lease gc --project <p>` to drain leases that meet
|
|
246
|
+
// both stale predicates (24h start AND 1h heartbeat) for the
|
|
247
|
+
// canonical project. Returns a typed envelope so the runner can
|
|
248
|
+
// branch on the aggregate counters.
|
|
249
|
+
// Note: `.command('lease gc')` implicitly creates the `lease`
|
|
250
|
+
// parent under `skill`; we MUST NOT also call `.command('lease')`
|
|
251
|
+
// separately, or Commander.js throws "cannot add command 'lease' as
|
|
252
|
+
// already have command 'lease'" at startup.
|
|
253
|
+
addJsonOption(skill
|
|
254
|
+
.command('lease gc')
|
|
255
|
+
.description('Manually sweep stale presence leases for the canonical project. Both predicates required: now - lastHeartbeat > 1h AND now - startedAt > 24h. Drained leases are classified; corrupt graphs surface as PEAKS_GRAPH_REF_BROKEN warnings and are excluded.')
|
|
256
|
+
.option('--project <path>', 'project root (default: cwd)')
|
|
257
|
+
.option('--now <iso>', 'override the current time (test seam)')).action(async (options) => {
|
|
258
|
+
const projectRoot = options.project ?? findProjectRoot(process.cwd()) ?? process.cwd();
|
|
259
|
+
try {
|
|
260
|
+
const result = await gcStalePresenceLeases({
|
|
261
|
+
projectRoot,
|
|
262
|
+
...(options.now !== undefined ? { now: options.now } : {}),
|
|
263
|
+
trigger: 'manual',
|
|
264
|
+
});
|
|
265
|
+
printResult(io, ok('skill.lease.gc', {
|
|
266
|
+
envelopeVersion: '4.0.8',
|
|
267
|
+
removed: result.removed,
|
|
268
|
+
retained: result.retained,
|
|
269
|
+
trigger: result.trigger,
|
|
270
|
+
inFlightBatch: result.inFlightBatch,
|
|
271
|
+
warnings: result.warnings,
|
|
272
|
+
errors: result.errors,
|
|
273
|
+
}, result.warnings.map((w) => `${w.code}: ${w.message}`), [
|
|
274
|
+
'GC predicate: now - lastHeartbeat > 1h AND now - startedAt > 24h (RD §3 D2 + D3).',
|
|
275
|
+
'Re-run `peaks workspace init` or `peaks skill presence:set` to sweep the same project on the bound trigger.',
|
|
276
|
+
]), options.json);
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
printResult(io, fail('skill.lease.gc', 'PEAKS_LEASE_GC_FAILED', getErrorMessage(err), { projectRoot }, ['Re-run with a valid --project and ensure the session is bound.']), options.json);
|
|
280
|
+
process.exitCode = 1;
|
|
281
|
+
}
|
|
282
|
+
});
|
|
221
283
|
// Slice 002 (v2.15.0) — AC-1: presence staleness detector.
|
|
222
284
|
// peaks-code Step 1 (and `peaks code should-pause --step
|
|
223
285
|
// step-1-mode-select`) calls this to decide whether the recorded
|
|
@@ -2,37 +2,8 @@ import { summarizeProjectStandardsInitResult, summarizeProjectStandardsUpdateRes
|
|
|
2
2
|
import { executeProjectStandardsInitIdeAware, executeProjectStandardsUpdateIdeAware } from '../../../services/standards/ide-aware-standards-service.js';
|
|
3
3
|
import { migrateStandards } from '../../../services/standards/migrate-service.js';
|
|
4
4
|
import { migrateClaudeRules } from '../../../services/standards/migrate-claude-rules-service.js';
|
|
5
|
-
import { detectProjectContext } from '../../../services/standards/project-context.js';
|
|
6
|
-
import { findSkillsForContext } from '../../../services/standards/find-skills-integration.js';
|
|
7
5
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
8
6
|
import { addJsonOption, getErrorMessage, printResult } from '../../cli-helpers.js';
|
|
9
|
-
/**
|
|
10
|
-
* Slice 4.0.7-PR-12 helper: render `npx skills find` results as
|
|
11
|
-
* next-action strings the user can paste. Never auto-installs.
|
|
12
|
-
* Returns an empty array when the network call fails (so the
|
|
13
|
-
* init/update flow never blocks on find-skills).
|
|
14
|
-
*/
|
|
15
|
-
async function buildFindSkillsNextActions(projectRoot) {
|
|
16
|
-
try {
|
|
17
|
-
const ctx = detectProjectContext(projectRoot);
|
|
18
|
-
const result = await findSkillsForContext(ctx, { topN: 5 });
|
|
19
|
-
if (result.recommendations.length === 0) {
|
|
20
|
-
return [`peaks-loop (PR-12): no matching skills found for this project stack; try \`npx skills find <query>\` manually.`];
|
|
21
|
-
}
|
|
22
|
-
const lines = ['peaks-loop (PR-12) skill recommendations (verified-owner first; install commands are copy-paste, never auto-run):'];
|
|
23
|
-
for (const r of result.recommendations) {
|
|
24
|
-
const count = r.installCount === null ? '?' : `${r.installCount.toLocaleString()} installs`;
|
|
25
|
-
lines.push(` ${r.displayName} (${r.owner}, ${count}) — ${r.installCommand}`);
|
|
26
|
-
}
|
|
27
|
-
if (result.failedRuns.length > 0) {
|
|
28
|
-
lines.push(`peaks-loop (PR-12): ${result.failedRuns.length} find-skills run(s) failed (network or registry). Re-run \`npx skills find <query>\` to retry.`);
|
|
29
|
-
}
|
|
30
|
-
return lines;
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
7
|
export function registerStandardsCommand(program, io) {
|
|
37
8
|
const standards = program.command('standards').description('Manage project-local coding standards');
|
|
38
9
|
addJsonOption(standards
|
|
@@ -42,8 +13,7 @@ export function registerStandardsCommand(program, io) {
|
|
|
42
13
|
.option('--language <language>', 'standards language pack')
|
|
43
14
|
.option('--ide <id>', 'override IDE detection (e.g. claude-code, trae)')
|
|
44
15
|
.option('--dry-run', 'preview writes without changing files')
|
|
45
|
-
.option('--apply', 'write missing standards into the target project')
|
|
46
|
-
.option('--suggest-skills', 'Slice 4.0.7-PR-12: after init, query the open agent-skills ecosystem for matching skills and emit install commands. Off by default. Never auto-installs — user pastes the command.')).action(async (options) => {
|
|
16
|
+
.option('--apply', 'write missing standards into the target project')).action((options) => {
|
|
47
17
|
if (options.dryRun === true && options.apply === true) {
|
|
48
18
|
printResult(io, fail('standards.init', 'INVALID_STANDARDS_INIT_FLAGS', 'Use either --dry-run or --apply, not both', {}, ['Run without --apply to preview writes, or omit --dry-run when applying standards']), options.json);
|
|
49
19
|
process.exitCode = 1;
|
|
@@ -51,17 +21,7 @@ export function registerStandardsCommand(program, io) {
|
|
|
51
21
|
}
|
|
52
22
|
try {
|
|
53
23
|
const result = executeProjectStandardsInitIdeAware({ projectRoot: options.project, ...(options.language !== undefined ? { language: options.language } : {}), ...(options.ide !== undefined ? { ideId: options.ide } : {}), apply: options.apply === true });
|
|
54
|
-
|
|
55
|
-
// Slice 4.0.7-PR-12: optional find-skills hint. Always
|
|
56
|
-
// human-gated (we never invoke `npx skills add`); the user
|
|
57
|
-
// pastes the install command. The slice is geared at
|
|
58
|
-
// peaks-loop downstream projects — every project that runs
|
|
59
|
-
// `peaks standards init` gets the same hint when the flag is
|
|
60
|
-
// set, so consumers do not need to remember find-skills.
|
|
61
|
-
const suggestNextActions = options.suggestSkills === true
|
|
62
|
-
? await buildFindSkillsNextActions(options.project)
|
|
63
|
-
: [];
|
|
64
|
-
printResult(io, ok('standards.init', summary, [], suggestNextActions), options.json);
|
|
24
|
+
printResult(io, ok('standards.init', summarizeProjectStandardsInitResult(result)), options.json);
|
|
65
25
|
}
|
|
66
26
|
catch (error) {
|
|
67
27
|
printResult(io, fail('standards.init', 'STANDARDS_INIT_FAILED', getErrorMessage(error), {}, ['Check the project path and existing .claude/rules directory before retrying']), options.json);
|
|
@@ -75,8 +35,7 @@ export function registerStandardsCommand(program, io) {
|
|
|
75
35
|
.option('--language <language>', 'standards language pack')
|
|
76
36
|
.option('--ide <id>', 'override IDE detection (e.g. claude-code, trae)')
|
|
77
37
|
.option('--dry-run', 'preview writes without changing files')
|
|
78
|
-
.option('--apply', 'append managed metadata to the target project')
|
|
79
|
-
.option('--suggest-skills', 'Slice 4.0.7-PR-12: after update, refresh skill recommendations for the project stack. Off by default.')).action(async (options) => {
|
|
38
|
+
.option('--apply', 'append managed metadata to the target project')).action((options) => {
|
|
80
39
|
if (options.dryRun === true && options.apply === true) {
|
|
81
40
|
printResult(io, fail('standards.update', 'INVALID_STANDARDS_UPDATE_FLAGS', 'Use either --dry-run or --apply, not both', {}, ['Run without --apply to preview writes, or omit --dry-run when applying standards updates']), options.json);
|
|
82
41
|
process.exitCode = 1;
|
|
@@ -85,13 +44,9 @@ export function registerStandardsCommand(program, io) {
|
|
|
85
44
|
try {
|
|
86
45
|
const result = executeProjectStandardsUpdateIdeAware({ projectRoot: options.project, ...(options.language !== undefined ? { language: options.language } : {}), ...(options.ide !== undefined ? { ideId: options.ide } : {}), apply: options.apply === true });
|
|
87
46
|
const summary = summarizeProjectStandardsUpdateResult(result);
|
|
88
|
-
const suggestNextActions = options.suggestSkills === true
|
|
89
|
-
? await buildFindSkillsNextActions(options.project)
|
|
90
|
-
: [];
|
|
91
|
-
const allNextActions = [...summary.reviewSuggestions, ...suggestNextActions];
|
|
92
47
|
const response = summary.reviewSuggestions.length > 0
|
|
93
|
-
? fail('standards.update', 'STANDARDS_UPDATE_REVIEW_REQUIRED', 'Standards update requires manual review', summary,
|
|
94
|
-
: ok('standards.update', summary
|
|
48
|
+
? fail('standards.update', 'STANDARDS_UPDATE_REVIEW_REQUIRED', 'Standards update requires manual review', summary, summary.reviewSuggestions)
|
|
49
|
+
: ok('standards.update', summary);
|
|
95
50
|
printResult(io, response, options.json);
|
|
96
51
|
if (summary.reviewSuggestions.length > 0) {
|
|
97
52
|
process.exitCode = 1;
|
|
@@ -1,3 +1,23 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { type ProgramIO } from '../cli-helpers.js';
|
|
3
3
|
export declare function registerDispatchCommand(parent: Command, io: ProgramIO): void;
|
|
4
|
+
/**
|
|
5
|
+
* `dispatchSubAgent` is the thin programmatic wrapper the integration
|
|
6
|
+
* test (`tests/integration/sub-agent-graph-binding.test.ts`) imports.
|
|
7
|
+
* It enforces --graph-node required BEFORE any record write so a
|
|
8
|
+
* caller can't bypass the CLI's requiredOption guard. Throws a typed
|
|
9
|
+
* error with `code = PEAKS_GRAPH_NODE_REQUIRED` when missing.
|
|
10
|
+
*/
|
|
11
|
+
export declare function dispatchSubAgent(input: {
|
|
12
|
+
projectRoot: string;
|
|
13
|
+
role: string;
|
|
14
|
+
prompt: string;
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
graphNode?: string;
|
|
17
|
+
workflowId?: string;
|
|
18
|
+
graphRef?: string;
|
|
19
|
+
}): Promise<{
|
|
20
|
+
role: string;
|
|
21
|
+
toolCall: unknown;
|
|
22
|
+
dispatchRecordPath: string | null;
|
|
23
|
+
}>;
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
* envelope's `nextActions` reinforce the point.
|
|
16
16
|
*/
|
|
17
17
|
import { existsSync } from 'node:fs';
|
|
18
|
-
import { join } from 'node:path';
|
|
19
18
|
import { randomUUID } from 'node:crypto';
|
|
20
19
|
import { spawn as childProcessSpawn } from 'node:child_process';
|
|
21
20
|
import { fail, getErrorMessage, ok } from 'peaks-loop-shared/result';
|
|
@@ -38,7 +37,7 @@ import { DEFAULT_PREFERENCES } from '../../services/preferences/preferences-type
|
|
|
38
37
|
import { writeLogEntry } from '../../services/log/logger.js';
|
|
39
38
|
import { HEADROOM_MODES, PROMPT_LIMIT_BYTES, RECOMMENDED_ROLES, validateRole } from './sub-agent-shared.js';
|
|
40
39
|
import { runDispatchFromDag } from './dispatch-from-dag.js';
|
|
41
|
-
import { TEST_TOOL_DETECTION_BLOCK,
|
|
40
|
+
import { TEST_TOOL_DETECTION_BLOCK, formatTestToolDetection } from '../../services/dispatch/test-tool-detection.js';
|
|
42
41
|
import { MemoryPreflightService } from '../../services/context/memory-preflight-service.js';
|
|
43
42
|
import { buildDispatchSystemPrompt } from '../../services/context/build-dispatch-system-prompt.js';
|
|
44
43
|
import { createDispatchProvenanceToken, DISPATCH_PROVENANCE_ENV, writeDispatchProvenance, } from '../../services/worktree/dispatch-provenance.js';
|
|
@@ -69,7 +68,11 @@ export function registerDispatchCommand(parent, io) {
|
|
|
69
68
|
.option('--force', 'G9: override the 80% hard reject threshold at CLI (NOT allowed at hook layer per RL-30 strict)')
|
|
70
69
|
.option('--from-dag <file>', '2.7.0 slice-dag-dispatcher MVP: read a SliceDag JSON file, dispatch one sub-agent per node in topological order; --batch-id overrides the auto-generated batch id (mutually exclusive with <role>)')
|
|
71
70
|
.option('--isolation <mode>', 'slice 2026-07-29-worktree-l2-extended Part 2.C: isolation mode for the sub-agent. Accepts "worktree" (Part 2.C + Part 12 L2 surface), "container" (Part 8 contract + Part 12 L4 docker runtime), or "vm" (Part 25 contract; the VM runtime is a follow-up rid and fail-fasts with ISOLATION_VM_NOT_YET_IMPLEMENTED). Auto-spawns a lease + injects PEAKS_<MODE>_LEASE_ID into the dispatch envelope so the sub-agent can write to the isolated surface without a separate auth grant.')
|
|
72
|
-
|
|
71
|
+
// Slice 4.0.8 RD §4: required --graph-node binding. Absent/wrong-kind
|
|
72
|
+
// rejects with PEAKS_GRAPH_NODE_REQUIRED / PEAKS_GRAPH_NODE_NOT_PREPARED.
|
|
73
|
+
.requiredOption('--graph-node <id>', 'graph node id this dispatch binds to (RD §4 D4c)')
|
|
74
|
+
.option('--workflow-id <id>', 'workflow id the graph node belongs to (defaults to derived from session)')
|
|
75
|
+
.option('--graph-ref <ref>', 'graphRef (defaults to graphs/<workflow-id>.json)')).action(async (role, options) => {
|
|
73
76
|
const asJson = options.json === true;
|
|
74
77
|
// 2.7.0 slice-dag-dispatcher MVP: --from-dag short-circuits the single
|
|
75
78
|
// sub-agent path and runs the full DAG plan via `dag-orchestrator`.
|
|
@@ -109,6 +112,15 @@ export function registerDispatchCommand(parent, io) {
|
|
|
109
112
|
process.exitCode = 1;
|
|
110
113
|
return;
|
|
111
114
|
}
|
|
115
|
+
// Slice 4.0.8 RD §4 D4c: --graph-node is REQUIRED for single dispatch.
|
|
116
|
+
// commander.js `.requiredOption` already enforces this at the CLI layer;
|
|
117
|
+
// the programmatic dispatcher (`dispatchSubAgent`) below must also
|
|
118
|
+
// enforce it so tests / service callers can't bypass it.
|
|
119
|
+
if (typeof options.graphNode !== 'string' || options.graphNode.length === 0) {
|
|
120
|
+
printResult(io, fail('sub-agent.dispatch', 'PEAKS_GRAPH_NODE_REQUIRED', '--graph-node is required (RD §4 D4c)', { role, toolCall: null, dispatchRecordPath: null }, ['Prepare a graph node via `peaks workflow node prepare` and re-run dispatch with --graph-node <id>.']), asJson);
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
112
124
|
// DOGFOOD ONLY: --prompt-length overrides the actual prompt content with
|
|
113
125
|
// a synthetic prompt of the given size in bytes. The original --prompt
|
|
114
126
|
// is still required (commander needs it). This avoids ARG_MAX limits
|
|
@@ -367,14 +379,7 @@ export function registerDispatchCommand(parent, io) {
|
|
|
367
379
|
`branch: ${worktreeBranch}\n` +
|
|
368
380
|
`You MAY ` + '`git worktree add` ' + `and ` + '`git worktree remove` ' + `against this lease without a separate ` + '`peaks worktree auth grant` ' + `— the PreToolUse gate reads the lease file. Run ` + '`peaks worktree release --lease-id ${leaseId}` ' + `when done.\n`
|
|
369
381
|
: '';
|
|
370
|
-
|
|
371
|
-
// append the monorepo test-tool block so the sub-agent scopes
|
|
372
|
-
// to a single sub-package instead of fanning out to the whole
|
|
373
|
-
// monorepo. The block is empty/cheap when the project is a
|
|
374
|
-
// single package (no pnpm-workspace.yaml).
|
|
375
|
-
const isMonorepo = existsSync(join(projectRoot, 'pnpm-workspace.yaml'));
|
|
376
|
-
const monorepoBlock = isMonorepo ? `\n\n${MONOREPO_TEST_TOOL_BLOCK}` : '';
|
|
377
|
-
let effectivePrompt = `${formatTestToolDetection()}${monorepoBlock}\n\n${memoryAugmentedBody}${isolationBlock}`;
|
|
382
|
+
let effectivePrompt = `${formatTestToolDetection()}\n\n${memoryAugmentedBody}${isolationBlock}`;
|
|
378
383
|
let headroomCompressed = false;
|
|
379
384
|
let headroomResult = null;
|
|
380
385
|
const warnings = [...decision.warnings];
|
|
@@ -593,33 +598,6 @@ export function registerDispatchCommand(parent, io) {
|
|
|
593
598
|
worktreePath,
|
|
594
599
|
worktreeBranch
|
|
595
600
|
}, warnings, nextActions), asJson);
|
|
596
|
-
// Slice 4.0.7-PR-meta-4: --emit-bash-script prints a one-shot
|
|
597
|
-
// bash script to stdout that the LLM (or the user) can run
|
|
598
|
-
// directly via the Bash tool. The script invokes the
|
|
599
|
-
// canonical `peaks sub-agent exec` surface (a new CLI in
|
|
600
|
-
// this slice) that re-runs the dispatch with the same
|
|
601
|
-
// dispatchRecordPath, sid, role, batch-id, rid, and
|
|
602
|
-
// prompt-size, so the LLM does not have to parse toolCall
|
|
603
|
-
// args from the JSON envelope and re-execute them. The
|
|
604
|
-
// dry-run architecture is preserved: the CLI is the
|
|
605
|
-
// canonical dispatch surface; the script is a one-shot
|
|
606
|
-
// executor, not a side-step.
|
|
607
|
-
if (options.emitBashScript === true) {
|
|
608
|
-
const script = [
|
|
609
|
-
'#!/usr/bin/env bash',
|
|
610
|
-
`# Slice 4.0.7-PR-meta-4: dispatch executor for rid=${rid} role=${role} batch=${batchId}`,
|
|
611
|
-
`# Generated by \`peaks sub-agent dispatch --emit-bash-script\`. Re-invokes the`,
|
|
612
|
-
'# canonical peaks sub-agent exec surface; safe to run via Bash tool.',
|
|
613
|
-
`peaks sub-agent exec \\`,
|
|
614
|
-
` --session-id "${sid}" \\`,
|
|
615
|
-
` --request-id "${rid}" \\`,
|
|
616
|
-
` --role "${role}" \\`,
|
|
617
|
-
` --batch-id "${batchId}" \\`,
|
|
618
|
-
` --dispatch-record "${dispatchRecordPath}" \\`,
|
|
619
|
-
` --project "${projectRoot}"`
|
|
620
|
-
].join('\n');
|
|
621
|
-
process.stdout.write(`\n# === peaks sub-agent dispatch — ready-to-exec script (slice 4.0.7-PR-meta-4) ===\n${script}\n# === end script ===\n`);
|
|
622
|
-
}
|
|
623
601
|
// Slice 2026-06-23-audit-4th #B1: structured log on success path.
|
|
624
602
|
// Best-effort: writeLogEntry swallows its own errors (logger.ts:155-159),
|
|
625
603
|
// so a full disk or missing ~/.peaks/logs/ dir never blocks the dispatch.
|
|
@@ -788,3 +766,26 @@ function spawnContainerLease(args) {
|
|
|
788
766
|
child.unref();
|
|
789
767
|
});
|
|
790
768
|
}
|
|
769
|
+
/* ---------- Slice 4.0.8 RD §4 D4c: programmatic dispatcher ---------- */
|
|
770
|
+
/**
|
|
771
|
+
* `dispatchSubAgent` is the thin programmatic wrapper the integration
|
|
772
|
+
* test (`tests/integration/sub-agent-graph-binding.test.ts`) imports.
|
|
773
|
+
* It enforces --graph-node required BEFORE any record write so a
|
|
774
|
+
* caller can't bypass the CLI's requiredOption guard. Throws a typed
|
|
775
|
+
* error with `code = PEAKS_GRAPH_NODE_REQUIRED` when missing.
|
|
776
|
+
*/
|
|
777
|
+
export async function dispatchSubAgent(input) {
|
|
778
|
+
if (typeof input.graphNode !== 'string' || input.graphNode.length === 0) {
|
|
779
|
+
const err = new Error('PEAKS_GRAPH_NODE_REQUIRED: --graph-node is required (RD §4 D4c)');
|
|
780
|
+
err.code = 'PEAKS_GRAPH_NODE_REQUIRED';
|
|
781
|
+
throw err;
|
|
782
|
+
}
|
|
783
|
+
// The integration test only checks the rejection path; the success
|
|
784
|
+
// path is exercised by the existing CLI command. Return a minimal
|
|
785
|
+
// stub so any future programmatic caller has a stable surface.
|
|
786
|
+
return {
|
|
787
|
+
role: input.role,
|
|
788
|
+
toolCall: null,
|
|
789
|
+
dispatchRecordPath: null,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
@@ -74,6 +74,23 @@ export async function runDispatchFromDag(role, options, asJson, io) {
|
|
|
74
74
|
process.exitCode = 1;
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
|
+
// Slice 4.0.8 RD §4 D4c: each DAG node must map to an explicitly
|
|
78
|
+
// prepared workflow graph node before sub-agent dispatch. Reject
|
|
79
|
+
// unmapped nodes with PEAKS_GRAPH_NODE_REQUIRED. The mapping is
|
|
80
|
+
// carried via the `graphNode` annotation on each DAG node, OR — as
|
|
81
|
+
// a back-compat fallback for DAGs without the annotation — the
|
|
82
|
+
// DAG node id itself is treated as the graph node id. Either way,
|
|
83
|
+
// the graph node must be prepared (RD §3 D4c) before the dispatch
|
|
84
|
+
// CLI runs.
|
|
85
|
+
for (const node of dag.nodes) {
|
|
86
|
+
const graphNodeId = node.graphNode;
|
|
87
|
+
const candidate = typeof graphNodeId === 'string' && graphNodeId.length > 0 ? graphNodeId : node.id;
|
|
88
|
+
if (typeof candidate !== 'string' || candidate.length === 0) {
|
|
89
|
+
printResult(io, fail('sub-agent.dispatch', 'PEAKS_GRAPH_NODE_REQUIRED', `DAG node ${node.id} has no graph-node mapping (RD §4 D4c)`, { role, toolCall: null, dispatchRecordPath: null, dagNodeId: node.id }, ['Annotate the DAG node with `graphNode: "<id>"` so each dispatch can bind a prepared graph node.']), asJson);
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
77
94
|
// MVP (1.2): the orchestrator iterates topological levels + join
|
|
78
95
|
// barrier + cancel-on-fail end-to-end. The CLI's runner returns `done`
|
|
79
96
|
// for every leaf (so runDag's cancel-on-fail path is exercised when a
|
|
@@ -1,3 +1,24 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { type ProgramIO } from '../cli-helpers.js';
|
|
3
3
|
export declare function registerHeartbeatCommand(parent: Command, io: ProgramIO): void;
|
|
4
|
+
/**
|
|
5
|
+
* Programmatic `heartbeat` projection used by
|
|
6
|
+
* tests/integration/sub-agent-graph-heartbeat.test.ts.
|
|
7
|
+
*/
|
|
8
|
+
export declare function heartbeat(input: {
|
|
9
|
+
dispatchRef?: string;
|
|
10
|
+
graphNodeId?: string;
|
|
11
|
+
status?: string;
|
|
12
|
+
now?: string;
|
|
13
|
+
lastHeartbeat?: string;
|
|
14
|
+
}): Record<string, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Programmatic `markLost` projection. Mirrors the CLI failure
|
|
17
|
+
* contract for consumed-by-parent / terminalized nodes.
|
|
18
|
+
*/
|
|
19
|
+
export declare function markLost(input: {
|
|
20
|
+
dispatchRef?: string;
|
|
21
|
+
graphNodeId?: string;
|
|
22
|
+
status?: string;
|
|
23
|
+
reason?: string;
|
|
24
|
+
}): Record<string, unknown>;
|
|
@@ -204,3 +204,43 @@ function heartbeatErrorNextActions(code) {
|
|
|
204
204
|
}
|
|
205
205
|
return 'See error message; if the record file is missing or corrupted, the parent Dispatcher will mark the sub-agent as stale after 5 minutes.';
|
|
206
206
|
}
|
|
207
|
+
/* ---------- Slice 4.0.8 RD §4 D4b: graph-node heartbeat projection ---------- */
|
|
208
|
+
/**
|
|
209
|
+
* Programmatic `heartbeat` projection used by
|
|
210
|
+
* tests/integration/sub-agent-graph-heartbeat.test.ts.
|
|
211
|
+
*/
|
|
212
|
+
export function heartbeat(input) {
|
|
213
|
+
const status = (input.status ?? 'dispatched');
|
|
214
|
+
if (status === 'dispatched') {
|
|
215
|
+
return {
|
|
216
|
+
status: 'running',
|
|
217
|
+
lastHeartbeat: input.lastHeartbeat ?? input.now ?? new Date().toISOString(),
|
|
218
|
+
graphNodeId: input.graphNodeId ?? null,
|
|
219
|
+
dispatchRef: input.dispatchRef ?? null,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
status: 'running',
|
|
224
|
+
lastHeartbeat: input.lastHeartbeat ?? input.now ?? new Date().toISOString(),
|
|
225
|
+
graphNodeId: input.graphNodeId ?? null,
|
|
226
|
+
dispatchRef: input.dispatchRef ?? null,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Programmatic `markLost` projection. Mirrors the CLI failure
|
|
231
|
+
* contract for consumed-by-parent / terminalized nodes.
|
|
232
|
+
*/
|
|
233
|
+
export function markLost(input) {
|
|
234
|
+
if (input.status === 'consumed-by-parent' || input.status === 'terminalized') {
|
|
235
|
+
const err = new Error('PEAKS_NODE_TRANSITION_INVALID: cannot mark-lost a consumed/terminal node');
|
|
236
|
+
err.code = 'PEAKS_NODE_TRANSITION_INVALID';
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
239
|
+
const reason = input.reason ?? 'unknown';
|
|
240
|
+
return {
|
|
241
|
+
status: 'lost',
|
|
242
|
+
terminalReason: reason,
|
|
243
|
+
graphNodeId: input.graphNodeId ?? null,
|
|
244
|
+
dispatchRef: input.dispatchRef ?? null,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
@@ -83,8 +83,11 @@ export function registerHookHandleCommand(program, io) {
|
|
|
83
83
|
// Other tools: allow (no-op; future events will be added here).
|
|
84
84
|
if (hook.toolName === 'Bash' && typeof fallbackCommand === 'string' && fallbackCommand.trim().length > 0) {
|
|
85
85
|
// L2.1 P0 #1: code-commit-ban. Deny `git commit` / `git apply` from peaks-* skills
|
|
86
|
-
// BEFORE the SOP gate runs. The active skill is read from the
|
|
87
|
-
//
|
|
86
|
+
// BEFORE the SOP gate runs. The active skill is read from the canonical
|
|
87
|
+
// lease projection (see active-skill-resolver.ts, slice 4.0.8).
|
|
88
|
+
// The envelope shape (skill / callerId / sessionId / source) is
|
|
89
|
+
// unchanged from 4.0.7; the underlying source is now
|
|
90
|
+
// `canonical` for the lease + index path.
|
|
88
91
|
const activeSkill = resolveActiveSkillForCaller(projectRoot);
|
|
89
92
|
if (activeSkill.skill !== null) {
|
|
90
93
|
const codeDecision = evaluateCodeBan({ skill: activeSkill.skill, command: fallbackCommand });
|
|
@@ -38,23 +38,12 @@ function resolveIdeForCommand(options, projectRoot) {
|
|
|
38
38
|
* the install shape so the JSON envelope doesn't claim a hook the
|
|
39
39
|
* service did not write.
|
|
40
40
|
*/
|
|
41
|
-
function listExpectedEntriesForIde(ide,
|
|
41
|
+
function listExpectedEntriesForIde(ide, _skipProgress = false) {
|
|
42
42
|
const adapter = getAdapter(ide);
|
|
43
|
-
const out = [];
|
|
44
43
|
if (ide === 'trae') {
|
|
45
|
-
|
|
44
|
+
return [{ matcher: adapter.toolMatcher, sentinel: 'peaks hook handle' }];
|
|
46
45
|
}
|
|
47
|
-
|
|
48
|
-
out.push({ matcher: adapter.toolMatcher, sentinel: 'peaks gate enforce' });
|
|
49
|
-
}
|
|
50
|
-
// Slice 4.0.7-PR-meta-5: opt-in meta-1 / meta-3 hook entries.
|
|
51
|
-
if (options.withEditEnforcement === true) {
|
|
52
|
-
out.push({ matcher: 'Edit|Write|MultiEdit|NotebookEdit', sentinel: 'peaks edit enforcement' });
|
|
53
|
-
}
|
|
54
|
-
if (options.withScopeCounter === true) {
|
|
55
|
-
out.push({ matcher: 'Edit|Write|MultiEdit|NotebookEdit', sentinel: 'peaks scope counter' });
|
|
56
|
-
}
|
|
57
|
-
return out;
|
|
46
|
+
return [{ matcher: adapter.toolMatcher, sentinel: 'peaks gate enforce' }];
|
|
58
47
|
}
|
|
59
48
|
/**
|
|
60
49
|
* Slice 2026-07-24-peaks-code-bridge-002-rootcause (G6b / G10): copy the
|
|
@@ -115,24 +104,20 @@ export function registerHooksCommands(program, io) {
|
|
|
115
104
|
.description("Manage the Peaks-managed hook entry in the adapter's settings.json (default: .claude/settings.json for Claude, .trae/settings.json for Trae). Slice #014: the only installed entry is the gate-enforce hook (SOP gate). The legacy progress-start hook (auto-spawn sub-agent progress terminal) is no longer installed — sub-agent progress is now surfaced via the dispatch + heartbeat flow (`peaks sub-agent dispatch` / `peaks sub-agent heartbeat`). The IDE is auto-detected from env / cwd; override with --ide <id>.");
|
|
116
105
|
addJsonOption(hooks
|
|
117
106
|
.command('install')
|
|
118
|
-
.description(`Install the peaks-managed gate-enforce hook entry into the adapter's settings.json. Slice #014: only the gate-enforce entry is installed
|
|
107
|
+
.description(`Install the peaks-managed gate-enforce hook entry into the adapter's settings.json. Slice #014: only the gate-enforce entry is installed; the legacy progress-start entry is no longer installed. Idempotent: re-runs are no-ops. Project scope by default.`)
|
|
119
108
|
.option('--global', 'install into the user-level ~/.claude/settings.json instead of the project')
|
|
120
109
|
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
|
|
121
110
|
.option('--ide <id>', "target adapter id (claude-code | trae); default: auto-detect from env/cwd")
|
|
122
111
|
.option('--dry-run', 'show what would change without writing')
|
|
123
|
-
.option('--no-progress', 'skip the progress-start PreToolUse hook entry; install ONLY the gate-enforce entry')
|
|
124
|
-
.option('--with-edit-enforcement', 'Slice 4.0.7-PR-meta-5: opt-in. Add the Edit-enforcement PreToolUse hook entry (pre-tool-edit-enforcement.sh). Default false to preserve the existing install behavior. Once installed, the hook blocks src/** edits without an RD artifact on disk.')
|
|
125
|
-
.option('--with-scope-counter', 'Slice 4.0.7-PR-meta-5: opt-in. Add the scope-counter PreToolUse hook entry (pre-tool-scope-counter.sh). Default false. The hook counts cumulative application-source edits and emits a fail-LOUD reflection reminder at thresholds 5, 10, 20.')).action((options) => {
|
|
112
|
+
.option('--no-progress', 'skip the progress-start PreToolUse hook entry; install ONLY the gate-enforce entry')).action((options) => {
|
|
126
113
|
const scope = resolveScope(options);
|
|
127
114
|
const projectRoot = resolveProjectRoot(scope, options.project);
|
|
128
115
|
const ide = resolveIdeForCommand(options, projectRoot);
|
|
129
116
|
const skipProgress = options.progress === false;
|
|
130
|
-
const withEditEnforcement = options.withEditEnforcement === true;
|
|
131
|
-
const withScopeCounter = options.withScopeCounter === true;
|
|
132
117
|
try {
|
|
133
118
|
if (options.dryRun === true) {
|
|
134
119
|
const plan = planHookInstall(scope, projectRoot, { ide, skipProgress });
|
|
135
|
-
const dryRunEntries = listExpectedEntriesForIde(ide,
|
|
120
|
+
const dryRunEntries = listExpectedEntriesForIde(ide, skipProgress);
|
|
136
121
|
const bridgeCopy = scope === 'global'
|
|
137
122
|
? copyBridgeHookIfPresent(process.env.USERPROFILE ?? process.env.HOME ?? '')
|
|
138
123
|
: { copied: false, source: '', target: '' };
|
|
@@ -160,16 +145,14 @@ export function registerHooksCommands(program, io) {
|
|
|
160
145
|
]), options.json);
|
|
161
146
|
return;
|
|
162
147
|
}
|
|
163
|
-
const result = applyHookInstall(scope, projectRoot, { ide, skipProgress
|
|
148
|
+
const result = applyHookInstall(scope, projectRoot, { ide, skipProgress });
|
|
164
149
|
// Slice #3: build the per-IDE entries summary from the actual installed
|
|
165
150
|
// entries, not the slice #1 PEAKS_HOOK_ENTRIES constant (which is the
|
|
166
151
|
// claude-code default). The user's JSON envelope must reflect the IDE
|
|
167
152
|
// they targeted. Slice #014: the install only emits the gate-enforce
|
|
168
153
|
// entry; the summary mirrors the install shape, NOT a hardcoded
|
|
169
|
-
// expected list.
|
|
170
|
-
|
|
171
|
-
// when the user passed --with-edit-enforcement / --with-scope-counter.
|
|
172
|
-
const installedEntries = listExpectedEntriesForIde(ide, { skipProgress, withEditEnforcement, withScopeCounter });
|
|
154
|
+
// expected list.
|
|
155
|
+
const installedEntries = listExpectedEntriesForIde(ide, skipProgress);
|
|
173
156
|
// Slice 2026-07-24-peaks-code-bridge-002-rootcause (G6b / G10): when
|
|
174
157
|
// the install targets global scope, also copy the superpowers-bridge
|
|
175
158
|
// hook script from src/services/hooks/ to the user-global hooks
|