peaks-loop 4.0.6 → 4.0.8
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 +48 -0
- package/dist/cli/commands/code-runtime-commands.js +30 -3
- package/dist/cli/commands/core/skill-command.js +75 -13
- package/dist/cli/commands/dispatch-commands.d.ts +20 -0
- package/dist/cli/commands/dispatch-commands.js +38 -1
- 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/loop-eval-commands.js +5 -2
- package/dist/cli/commands/sub-agent-shared.d.ts +9 -0
- 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/enforcers/active-skill-resolver.d.ts +25 -9
- package/dist/services/audit/enforcers/active-skill-resolver.js +83 -21
- package/dist/services/code/auto-compact-orchestrator.d.ts +35 -2
- package/dist/services/code/auto-compact-orchestrator.js +29 -9
- package/dist/services/dispatch/dispatch-record-writer.d.ts +25 -1
- package/dist/services/dispatch/dispatch-record-writer.js +85 -15
- 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/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/observability/observability-service.d.ts +2 -2
- 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 +3 -1
- package/dist/services/session/index.js +7 -1
- 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/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/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 +4 -4
- package/skills/peaks-code/references/completion-handoff.md +1 -1
- package/skills/peaks-code/references/skill-presence-and-title.md +1 -1
|
@@ -108,19 +108,29 @@ export function evaluateCompactTrigger(ratio, mode = 'standard') {
|
|
|
108
108
|
*/
|
|
109
109
|
export function evaluateAutoCompactDecision(input) {
|
|
110
110
|
const trigger = evaluateCompactTrigger(input.ratio, input.mode ?? 'standard');
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
// Normalize both `inFlightBatch` and `inflightBatch` (boolean | probe)
|
|
112
|
+
// into a single `InFlightBatchProbe` shape. The probe reads
|
|
113
|
+
// `hasInFlightBatch`; the boolean reads truthiness.
|
|
114
|
+
const rawProbe = input.inFlightBatch ?? input.inflightBatch;
|
|
115
|
+
const probe = typeof rawProbe === 'boolean'
|
|
116
|
+
? { hasInFlightBatch: rawProbe }
|
|
117
|
+
: rawProbe;
|
|
118
|
+
if (trigger.kind === 'none') {
|
|
119
|
+
return { shouldCompact: false, reason: 'below-threshold', trigger, action: 'ok' };
|
|
120
|
+
}
|
|
121
|
+
if (trigger.kind === 'soft-warn') {
|
|
122
|
+
return { shouldCompact: false, reason: 'below-threshold', trigger, action: 'soft-warn' };
|
|
113
123
|
}
|
|
114
124
|
if (trigger.kind === 'red-line') {
|
|
115
125
|
// Red line: ignore in-flight batch — synchronous dispatch wins.
|
|
116
|
-
return { shouldCompact: true, reason: 'red-line', trigger };
|
|
126
|
+
return { shouldCompact: true, reason: 'red-line', trigger, action: 'red-line' };
|
|
117
127
|
}
|
|
118
128
|
// pre-compact zone (0.85 ≤ ratio < 0.95): honor D6.e in-flight deferral.
|
|
119
|
-
if (
|
|
120
|
-
return { shouldCompact: false, reason: 'in-flight-batch', trigger };
|
|
129
|
+
if (probe?.hasInFlightBatch === true) {
|
|
130
|
+
return { shouldCompact: false, reason: 'in-flight-batch', trigger, action: 'defer' };
|
|
121
131
|
}
|
|
122
132
|
if (input.force) {
|
|
123
|
-
return { shouldCompact: true, reason: 'pre-compact', trigger };
|
|
133
|
+
return { shouldCompact: true, reason: 'pre-compact', trigger, action: 'auto-compact-now' };
|
|
124
134
|
}
|
|
125
135
|
// Slice 2026-07-31-rid-mac-transcript-estimate-trigger: transcript-estimate
|
|
126
136
|
// is the ONLY signal available on Mac Claude Code (no env-var, no statusline
|
|
@@ -131,9 +141,9 @@ export function evaluateAutoCompactDecision(input) {
|
|
|
131
141
|
// source is present (`claude-code-env` would have been P1, `statusline-poll`
|
|
132
142
|
// P2, `user-overridden` P4) — Mac's only signal is `transcript-estimate`.
|
|
133
143
|
if (input.source === 'transcript-estimate' && input.ratio >= AUTO_COMPACT_PRE_COMPACT_RATIO)
|
|
134
|
-
return { shouldCompact: true, reason: 'pre-compact', trigger };
|
|
144
|
+
return { shouldCompact: true, reason: 'pre-compact', trigger, action: 'auto-compact-now' };
|
|
135
145
|
// Default: peaks-loop drives pre-compact autonomously.
|
|
136
|
-
return { shouldCompact: true, reason: 'pre-compact', trigger };
|
|
146
|
+
return { shouldCompact: true, reason: 'pre-compact', trigger, action: 'auto-compact-now' };
|
|
137
147
|
}
|
|
138
148
|
/**
|
|
139
149
|
* Build the convergence plan that D7's post-compact-detect will read
|
|
@@ -514,7 +524,17 @@ export async function runAutoCompact(input) {
|
|
|
514
524
|
});
|
|
515
525
|
const decision = evaluateAutoCompactDecision({
|
|
516
526
|
ratio: probe.ratio,
|
|
517
|
-
|
|
527
|
+
// Slice 4.0.8 (D4d): production `inFlightBatch` MUST come from
|
|
528
|
+
// the graph probe (see `workflow-inflight-probe.ts`). The
|
|
529
|
+
// legacy boolean is preserved as a TEST-ONLY seam: when
|
|
530
|
+
// `probeInflightBatch` is supplied, we call it and pass the
|
|
531
|
+
// result as a `InFlightBatchProbe` so the pure decision function
|
|
532
|
+
// sees a graph-backed value. When only the boolean is
|
|
533
|
+
// supplied, we treat it as the test seam (the CLI gates it
|
|
534
|
+
// behind `PEAKS_TEST_SEAM === '1'`).
|
|
535
|
+
inFlightBatch: input.probeInflightBatch !== undefined
|
|
536
|
+
? { hasInFlightBatch: input.probeInflightBatch() }
|
|
537
|
+
: input.inFlightBatch,
|
|
518
538
|
force: input.force,
|
|
519
539
|
bypassRedLine: input.bypassRedLine,
|
|
520
540
|
mode,
|
|
@@ -43,7 +43,7 @@ export interface DispatchRecord {
|
|
|
43
43
|
* truth for "this record is v3.1-form"; readers check
|
|
44
44
|
* `version === '3.1'` for forward-compatible dispatching.
|
|
45
45
|
*/
|
|
46
|
-
readonly version: '
|
|
46
|
+
readonly version: '4.0.0';
|
|
47
47
|
readonly createdAt: string;
|
|
48
48
|
readonly completedAt: string | null;
|
|
49
49
|
readonly outcome: DispatchOutcome;
|
|
@@ -120,6 +120,22 @@ export interface DispatchRecord {
|
|
|
120
120
|
* merge transcript.
|
|
121
121
|
*/
|
|
122
122
|
readonly mergeBackAttempts: number;
|
|
123
|
+
/**
|
|
124
|
+
* Slice 4.0.8 RD §4 D4c (presence-lease-graph): the workflow id +
|
|
125
|
+
* graph node id + graphRef this dispatch is bound to. Persisted
|
|
126
|
+
* directly in the dispatch record (NOT in a sidecar) so the
|
|
127
|
+
* envelope-writer `markCompleted` can auto-transition the bound
|
|
128
|
+
* graph node to `envelope-received` with `ackStatus=pending` in
|
|
129
|
+
* one protected update.
|
|
130
|
+
*
|
|
131
|
+
* The schema bump from `3.2 → 4.0.0` is BREAKING in the sense
|
|
132
|
+
* that the literal type is narrowed. The optional `?` keeps back-
|
|
133
|
+
* compat for old records that pre-date the binding (the
|
|
134
|
+
* `upgradeRecord` reader defaults them to `null`).
|
|
135
|
+
*/
|
|
136
|
+
readonly workflowId: string | null;
|
|
137
|
+
readonly graphNodeId: string | null;
|
|
138
|
+
readonly graphRef: string | null;
|
|
123
139
|
}
|
|
124
140
|
/** Input for the initial write. */
|
|
125
141
|
export type WriteInitialDispatchInput = {
|
|
@@ -148,6 +164,14 @@ export type WriteInitialDispatchInput = {
|
|
|
148
164
|
* time when `--isolation` is requested.
|
|
149
165
|
*/
|
|
150
166
|
isolationStartedAt?: string | null;
|
|
167
|
+
/**
|
|
168
|
+
* Slice 4.0.8: workflow graph binding for the dispatch. Defaults
|
|
169
|
+
* to `null` so a non-graph dispatch (legacy CLI flow, ad-hoc
|
|
170
|
+
* dispatch) still writes a v4.0.0 record.
|
|
171
|
+
*/
|
|
172
|
+
workflowId?: string | null;
|
|
173
|
+
graphNodeId?: string | null;
|
|
174
|
+
graphRef?: string | null;
|
|
151
175
|
};
|
|
152
176
|
/** Heartbeat write input. */
|
|
153
177
|
export type AppendHeartbeatInput = {
|
|
@@ -44,7 +44,7 @@ export function writeInitialDispatchRecord(input) {
|
|
|
44
44
|
const path = dispatchRecordPath(projectRoot, sessionId, requestId, now());
|
|
45
45
|
const safePath = assertSafeDispatchRecordPath(path, projectRoot);
|
|
46
46
|
const record = {
|
|
47
|
-
version: '
|
|
47
|
+
version: '4.0.0',
|
|
48
48
|
createdAt: now().toISOString(),
|
|
49
49
|
completedAt: null,
|
|
50
50
|
outcome: 'no-execution',
|
|
@@ -91,7 +91,14 @@ export function writeInitialDispatchRecord(input) {
|
|
|
91
91
|
// New records start with empty serviceKill and zero attempts;
|
|
92
92
|
// the merge-back-runner (Task 9) populates them in place.
|
|
93
93
|
serviceKill: [],
|
|
94
|
-
mergeBackAttempts: 0
|
|
94
|
+
mergeBackAttempts: 0,
|
|
95
|
+
// Slice 4.0.8: workflow graph binding (RD §4 D4c). Defaults to
|
|
96
|
+
// `null` for legacy / ad-hoc dispatches that do not bind a graph
|
|
97
|
+
// node; v4.0.0 schema is structural (required field), so a `null`
|
|
98
|
+
// is the explicit "no binding" state.
|
|
99
|
+
workflowId: typeof input.workflowId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(input.workflowId) ? input.workflowId : null,
|
|
100
|
+
graphNodeId: typeof input.graphNodeId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(input.graphNodeId) ? input.graphNodeId : null,
|
|
101
|
+
graphRef: typeof input.graphRef === 'string' && input.graphRef.length > 0 ? input.graphRef : null,
|
|
95
102
|
};
|
|
96
103
|
writeAtomic(safePath, record);
|
|
97
104
|
// Slice 2026-06-23-audit-4th #A4: register the path in the
|
|
@@ -425,6 +432,68 @@ export function markCompleted(input) {
|
|
|
425
432
|
writeAtomic(input.recordPath, next);
|
|
426
433
|
return { record: next };
|
|
427
434
|
});
|
|
435
|
+
// Slice 4.0.8 RD §4 D4c: after validating the artifact/envelope
|
|
436
|
+
// association with `dispatchRef`, automatically transition the
|
|
437
|
+
// bound graph node to `envelope-received` with
|
|
438
|
+
// `ackStatus=pending`. This is the auto-transition the parent
|
|
439
|
+
// ack protocol relies on — the canonical dispatch chain is
|
|
440
|
+
// `prepare -> dispatched -> running -> envelope-received -> consumed-by-parent`.
|
|
441
|
+
// The transition runs through `workflow-node-lifecycle.writeEnvelope`
|
|
442
|
+
// so the same typed error contract (PEAKS_ENVELOPE_GRAPH_MISMATCH,
|
|
443
|
+
// PEAKS_GRAPH_REF_BROKEN, etc.) is reused. Failures are swallowed
|
|
444
|
+
// (best-effort; the dispatch record itself is the source of truth
|
|
445
|
+
// and the transition is observable through the graph store).
|
|
446
|
+
if (result.record.workflowId !== null && result.record.graphNodeId !== null && result.record.graphRef !== null) {
|
|
447
|
+
try {
|
|
448
|
+
// Lazy ESM dynamic import: the graph lifecycle service is not
|
|
449
|
+
// on the dispatch hot path; it would be wasteful to import it
|
|
450
|
+
// for non-graph-bound dispatches. ESM dynamic import returns
|
|
451
|
+
// a promise; we do not await (best-effort) — the dispatch
|
|
452
|
+
// record write is the load-bearing artifact, and the graph
|
|
453
|
+
// node transition is observable to the next CLI call.
|
|
454
|
+
void (async () => {
|
|
455
|
+
try {
|
|
456
|
+
const lifecycleMod = await import('../workflow/workflow-node-lifecycle.js');
|
|
457
|
+
const storeMod = await import('../workflow/workflow-graph-store.js');
|
|
458
|
+
const sessionRoot = (() => {
|
|
459
|
+
try {
|
|
460
|
+
return storeMod.graphPathFor({
|
|
461
|
+
projectRoot: input.projectRoot ?? '',
|
|
462
|
+
sessionId: result.record.sessionId,
|
|
463
|
+
graphRef: result.record.graphRef ?? '',
|
|
464
|
+
workflowId: result.record.workflowId ?? '',
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
})();
|
|
471
|
+
if (sessionRoot === null)
|
|
472
|
+
return;
|
|
473
|
+
const graph = storeMod.readGraph({
|
|
474
|
+
projectRoot: input.projectRoot ?? '',
|
|
475
|
+
sessionId: result.record.sessionId,
|
|
476
|
+
graphRef: result.record.graphRef ?? '',
|
|
477
|
+
workflowId: result.record.workflowId ?? '',
|
|
478
|
+
});
|
|
479
|
+
const node = graph.nodes.find((n) => n.id === result.record.graphNodeId);
|
|
480
|
+
if (node === undefined)
|
|
481
|
+
return;
|
|
482
|
+
// Use the dispatch record's path as the dispatchRef. The
|
|
483
|
+
// record itself is the load-bearing artifact; the
|
|
484
|
+
// graph-node transition is a derived side-effect.
|
|
485
|
+
const dispatchRef = input.recordPath;
|
|
486
|
+
lifecycleMod.writeEnvelope({
|
|
487
|
+
graphNode: node,
|
|
488
|
+
dispatchRef,
|
|
489
|
+
envelopeDispatchRef: dispatchRef,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
catch { /* best-effort graph transition */ }
|
|
493
|
+
})();
|
|
494
|
+
}
|
|
495
|
+
catch { /* best-effort */ }
|
|
496
|
+
}
|
|
428
497
|
// Slice 2026-06-23-audit-4th #A4: update the active-dispatches
|
|
429
498
|
// index. Best-effort (the on-disk record is the source of truth);
|
|
430
499
|
// we only attempt the update when the trusted projectRoot is
|
|
@@ -549,18 +618,13 @@ function upgradeRecord(parsed) {
|
|
|
549
618
|
throw new Error('Dispatch record root must be an object');
|
|
550
619
|
}
|
|
551
620
|
const obj = parsed;
|
|
552
|
-
// Slice
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
// upgraded to v3.1 on read. The forward-compat check rejects
|
|
556
|
-
// records with an unknown version string (which would
|
|
557
|
-
// indicate either a much-older build or a much-newer one
|
|
558
|
-
// with a schema we have not implemented). v1 records
|
|
559
|
-
// (pre-audit-trail) must be regenerated.
|
|
621
|
+
// Slice 4.0.8: 3.2 → 4.0.0 schema bump. The literal type narrows
|
|
622
|
+
// to '4.0.0' but legacy v3.2 / v3.1 / 3 / 2 / 1 records are
|
|
623
|
+
// accepted transparently and upgraded on read.
|
|
560
624
|
const rawVersion = obj.version;
|
|
561
|
-
if (rawVersion !== '3.2' && rawVersion !== '3.1' && rawVersion !== 3 && rawVersion !== 2 && rawVersion !== 1) {
|
|
562
|
-
throw new Error(`Dispatch record version mismatch: expected '3.2', '3.1', 3, 2, or 1, got ${JSON.stringify(rawVersion)}. ` +
|
|
563
|
-
'The v1 →
|
|
625
|
+
if (rawVersion !== '4.0.0' && rawVersion !== '3.2' && rawVersion !== '3.1' && rawVersion !== 3 && rawVersion !== 2 && rawVersion !== 1) {
|
|
626
|
+
throw new Error(`Dispatch record version mismatch: expected '4.0.0', '3.2', '3.1', 3, 2, or 1, got ${JSON.stringify(rawVersion)}. ` +
|
|
627
|
+
'The v1 → v4.0.0 migration is in-file; records from much older or newer builds must be regenerated.');
|
|
564
628
|
}
|
|
565
629
|
const role = stringField(obj, 'role');
|
|
566
630
|
const requestId = stringField(obj, 'requestId');
|
|
@@ -602,7 +666,7 @@ function upgradeRecord(parsed) {
|
|
|
602
666
|
? obj.batchId
|
|
603
667
|
: 'legacy-batch';
|
|
604
668
|
return {
|
|
605
|
-
version: '
|
|
669
|
+
version: '4.0.0',
|
|
606
670
|
createdAt,
|
|
607
671
|
completedAt,
|
|
608
672
|
outcome,
|
|
@@ -652,7 +716,13 @@ function upgradeRecord(parsed) {
|
|
|
652
716
|
: [],
|
|
653
717
|
mergeBackAttempts: typeof obj.mergeBackAttempts === 'number' && Number.isFinite(obj.mergeBackAttempts) && obj.mergeBackAttempts >= 0
|
|
654
718
|
? Math.floor(obj.mergeBackAttempts)
|
|
655
|
-
: 0
|
|
719
|
+
: 0,
|
|
720
|
+
// Slice 4.0.8: 3.2 → 4.0.0 migration. v3.2 records on disk
|
|
721
|
+
// pre-date the workflow-graph binding; default all three
|
|
722
|
+
// fields to `null` so a legacy record upgrades transparently.
|
|
723
|
+
workflowId: typeof obj.workflowId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(obj.workflowId) ? obj.workflowId : null,
|
|
724
|
+
graphNodeId: typeof obj.graphNodeId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(obj.graphNodeId) ? obj.graphNodeId : null,
|
|
725
|
+
graphRef: typeof obj.graphRef === 'string' ? obj.graphRef : null
|
|
656
726
|
};
|
|
657
727
|
}
|
|
658
728
|
function isObject(v) {
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
* Check: skill presence current + freshness
|
|
3
3
|
* (`skill-presence:current` and `skill-presence:freshness`).
|
|
4
4
|
*
|
|
5
|
+
* Slice 4.0.8 (RD §3 + §4 D3): the diagnostic also surfaces
|
|
6
|
+
* stale / lost / broken-graph leases by running a `dry-run` GC
|
|
7
|
+
* summary through `gcStalePresenceLeases`. The summary carries
|
|
8
|
+
* `removed` / `retained` counts plus per-lease warnings (typed
|
|
9
|
+
* `PEAKS_GRAPH_REF_BROKEN`, `PEAKS_GRAPH_CORRUPTED`); the doctor
|
|
10
|
+
* surfaces them so the operator can run `peaks workspace reconcile`
|
|
11
|
+
* to repair.
|
|
12
|
+
*
|
|
5
13
|
* Two checks sharing one read of the `presence` context field.
|
|
6
14
|
* - `skill-presence:current` is informational: passes when the
|
|
7
15
|
* presence is wired, with a message describing the active
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
* Check: skill presence current + freshness
|
|
3
3
|
* (`skill-presence:current` and `skill-presence:freshness`).
|
|
4
4
|
*
|
|
5
|
+
* Slice 4.0.8 (RD §3 + §4 D3): the diagnostic also surfaces
|
|
6
|
+
* stale / lost / broken-graph leases by running a `dry-run` GC
|
|
7
|
+
* summary through `gcStalePresenceLeases`. The summary carries
|
|
8
|
+
* `removed` / `retained` counts plus per-lease warnings (typed
|
|
9
|
+
* `PEAKS_GRAPH_REF_BROKEN`, `PEAKS_GRAPH_CORRUPTED`); the doctor
|
|
10
|
+
* surfaces them so the operator can run `peaks workspace reconcile`
|
|
11
|
+
* to repair.
|
|
12
|
+
*
|
|
5
13
|
* Two checks sharing one read of the `presence` context field.
|
|
6
14
|
* - `skill-presence:current` is informational: passes when the
|
|
7
15
|
* presence is wired, with a message describing the active
|
|
@@ -56,7 +64,7 @@ function run({ options, presence }) {
|
|
|
56
64
|
checks.push({
|
|
57
65
|
id: 'skill-presence:freshness',
|
|
58
66
|
ok: false,
|
|
59
|
-
message: `Skill presence ${presence.skill} is stale (set ${presence.setAt}, ~${ageHours}h ago); run peaks skill presence:clear if the role has ended`
|
|
67
|
+
message: `Skill presence ${presence.skill} is stale (set ${presence.setAt}, ~${ageHours}h ago); run peaks skill presence:clear if the role has ended, or \`peaks skill lease gc\` to drain the canonical lease`
|
|
60
68
|
});
|
|
61
69
|
}
|
|
62
70
|
else {
|
|
@@ -12,5 +12,11 @@ export type PresenceMarkerWarning = (typeof PRESENCE_MARKER_WARNING)[number];
|
|
|
12
12
|
export declare const PRESENCE_MARKER_WARNING: readonly ["Peaks skill context may have been lost from this conversation; please re-invoke /peaks-<skill>."];
|
|
13
13
|
/**
|
|
14
14
|
* Pure read-only presence-marker detection. No I/O side effects.
|
|
15
|
+
*
|
|
16
|
+
* Slice 4.0.8: the canonical 4.0.7 behavior is preserved (read legacy
|
|
17
|
+
* `active-skill.json` or `.peaks/.active-skill.json`, project the
|
|
18
|
+
* `Peaks-Loop Skill:` marker). The new diagnostic for a broken
|
|
19
|
+
* canonical graph is surfaced via the `warning` field, NOT swallowed,
|
|
20
|
+
* so the statusline / hook / doctor consumers can render it.
|
|
15
21
|
*/
|
|
16
22
|
export declare function detectPresenceMarker(input: DetectPresenceMarkerInput): DetectPresenceMarkerResult;
|
|
@@ -73,6 +73,41 @@ function readPresenceFile(absolutePath) {
|
|
|
73
73
|
return null;
|
|
74
74
|
return { skill: skillMatch[1] };
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Slice 4.0.8: surface a typed `PEAKS_GRAPH_REF_BROKEN` warning
|
|
78
|
+
* when the canonical presence lease / index points at a missing or
|
|
79
|
+
* corrupt graph. The legacy `active-skill.json` walk never
|
|
80
|
+
* inspected graphRef; in 4.0.8 we surface the broken graph in the
|
|
81
|
+
* `warnings` array so downstream consumers (statusline, doctor,
|
|
82
|
+
* hooks) can render the diagnostic instead of silently rendering
|
|
83
|
+
* "active" for a half-wired presence.
|
|
84
|
+
*/
|
|
85
|
+
function tryDetectBrokenGraph(projectRoot) {
|
|
86
|
+
try {
|
|
87
|
+
const presenceIndexDir = resolve(projectRoot, '.peaks', '_runtime');
|
|
88
|
+
if (!existsSync(presenceIndexDir))
|
|
89
|
+
return null;
|
|
90
|
+
// Best-effort: walk a single directory level for the legacy
|
|
91
|
+
// `active-skill.json` indicator. A broken graphRef would be
|
|
92
|
+
// surfaced by the canonical lease service (PEAKS_GRAPH_REF_BROKEN
|
|
93
|
+
// is the typed error), but the marker detector is a *read* —
|
|
94
|
+
// we don't import the lease service here. The diagnostic is
|
|
95
|
+
// best-effort: we report the index file's `graphRef` field
|
|
96
|
+
// when it names a missing file.
|
|
97
|
+
const legacyPath = resolve(projectRoot, '.peaks', '_runtime', 'active-skill.json');
|
|
98
|
+
if (!existsSync(legacyPath))
|
|
99
|
+
return null;
|
|
100
|
+
const raw = readFileSync(legacyPath, 'utf8');
|
|
101
|
+
const parsed = JSON.parse(raw);
|
|
102
|
+
if (typeof parsed.graphRef !== 'string')
|
|
103
|
+
return null;
|
|
104
|
+
const graphPath = resolve(projectRoot, '.peaks', '_runtime', parsed.graphRef);
|
|
105
|
+
if (!existsSync(graphPath))
|
|
106
|
+
return 'PEAKS_GRAPH_REF_BROKEN';
|
|
107
|
+
}
|
|
108
|
+
catch { /* swallow — diagnostic only */ }
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
76
111
|
function readPresenceBackCompat(project) {
|
|
77
112
|
const projectRoot = resolve(project);
|
|
78
113
|
const canonicalPath = resolve(projectRoot, PRESENCE_CANONICAL_PATH);
|
|
@@ -92,6 +127,12 @@ function messageHasMarker(message) {
|
|
|
92
127
|
}
|
|
93
128
|
/**
|
|
94
129
|
* Pure read-only presence-marker detection. No I/O side effects.
|
|
130
|
+
*
|
|
131
|
+
* Slice 4.0.8: the canonical 4.0.7 behavior is preserved (read legacy
|
|
132
|
+
* `active-skill.json` or `.peaks/.active-skill.json`, project the
|
|
133
|
+
* `Peaks-Loop Skill:` marker). The new diagnostic for a broken
|
|
134
|
+
* canonical graph is surfaced via the `warning` field, NOT swallowed,
|
|
135
|
+
* so the statusline / hook / doctor consumers can render it.
|
|
95
136
|
*/
|
|
96
137
|
export function detectPresenceMarker(input) {
|
|
97
138
|
const project = input.project;
|
|
@@ -100,10 +141,17 @@ export function detectPresenceMarker(input) {
|
|
|
100
141
|
if (presence === null) {
|
|
101
142
|
return { active: false, markerFound: false };
|
|
102
143
|
}
|
|
144
|
+
const brokenGraph = tryDetectBrokenGraph(project);
|
|
103
145
|
const markerFound = messageHasMarker(message);
|
|
104
146
|
if (markerFound) {
|
|
147
|
+
if (brokenGraph !== null) {
|
|
148
|
+
return { active: true, skill: presence.skill, markerFound: true, warning: `Peaks presence active but graph is broken (${brokenGraph}); run \`peaks workspace reconcile\` to repair.` };
|
|
149
|
+
}
|
|
105
150
|
return { active: true, skill: presence.skill, markerFound: true };
|
|
106
151
|
}
|
|
152
|
+
if (brokenGraph !== null) {
|
|
153
|
+
return { active: true, skill: presence.skill, markerFound: false, warning: `Peaks presence active but graph is broken (${brokenGraph}); run \`peaks workspace reconcile\` to repair.` };
|
|
154
|
+
}
|
|
107
155
|
return {
|
|
108
156
|
active: true,
|
|
109
157
|
skill: presence.skill,
|
|
@@ -100,4 +100,24 @@ export const CLAUDE_CODE_ADAPTER = {
|
|
|
100
100
|
installStrategy: 'symlink',
|
|
101
101
|
envVarOverride: 'PEAKS_CLAUDE_SKILLS_DIR',
|
|
102
102
|
},
|
|
103
|
+
// Slice 4.0.8 RD §5: Claude Code resolves PEAKS_CALLER_ID (override) →
|
|
104
|
+
// CLAUDE_CODE_SESSION_ID. Empty/invalid → typed PEAKS_CALLER_NOT_RESOLVED.
|
|
105
|
+
resolveCallerId: (env) => {
|
|
106
|
+
const e = env ?? process.env;
|
|
107
|
+
const override = e.PEAKS_CALLER_ID;
|
|
108
|
+
if (typeof override === 'string' && override.trim().length > 0) {
|
|
109
|
+
const trimmed = override.trim();
|
|
110
|
+
if (/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed))
|
|
111
|
+
return trimmed;
|
|
112
|
+
}
|
|
113
|
+
const v = e.CLAUDE_CODE_SESSION_ID;
|
|
114
|
+
if (typeof v === 'string' && v.trim().length > 0) {
|
|
115
|
+
const trimmed = v.trim();
|
|
116
|
+
if (/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed))
|
|
117
|
+
return trimmed;
|
|
118
|
+
}
|
|
119
|
+
const err = new Error('PEAKS_CALLER_NOT_RESOLVED: no Claude Code session id available');
|
|
120
|
+
err.code = 'PEAKS_CALLER_NOT_RESOLVED';
|
|
121
|
+
throw err;
|
|
122
|
+
},
|
|
103
123
|
};
|
|
@@ -86,7 +86,26 @@ export const CODEX_ADAPTER = {
|
|
|
86
86
|
// return a clear "not supported" stderr message (slice #008 P-5
|
|
87
87
|
// capability-check contract preserved).
|
|
88
88
|
statusline: false,
|
|
89
|
-
}
|
|
89
|
+
},
|
|
90
|
+
// Slice 4.0.8 RD §5: Codex vendor signal reserved; fail closed.
|
|
91
|
+
resolveCallerId: (env) => {
|
|
92
|
+
const e = env ?? process.env;
|
|
93
|
+
const override = e.PEAKS_CALLER_ID;
|
|
94
|
+
if (typeof override === 'string' && override.trim().length > 0) {
|
|
95
|
+
const trimmed = override.trim();
|
|
96
|
+
if (/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed))
|
|
97
|
+
return trimmed;
|
|
98
|
+
}
|
|
99
|
+
const candidate = e.CODEX_SESSION_ID;
|
|
100
|
+
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
101
|
+
const trimmed = candidate.trim();
|
|
102
|
+
if (/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed))
|
|
103
|
+
return trimmed;
|
|
104
|
+
}
|
|
105
|
+
const err = new Error('PEAKS_CALLER_NOT_RESOLVED: Codex vendor signal reserved');
|
|
106
|
+
err.code = 'PEAKS_CALLER_NOT_RESOLVED';
|
|
107
|
+
throw err;
|
|
108
|
+
},
|
|
90
109
|
// Standards: UNVERIFIED — see slice #013+ (Codex real-install dogfood for
|
|
91
110
|
// the `standardsProfile` and `skillInstall` fields). Until then, `peaks
|
|
92
111
|
// standards init` on a Codex-detected project falls back to the Claude
|
|
@@ -73,7 +73,7 @@ export const CURSOR_ADAPTER = {
|
|
|
73
73
|
// Cursor has a statusline UI (Cmd+Shift+P → "Cursor: Open Status Bar")
|
|
74
74
|
// that can host peaks statusline output, so opt in to the capability.
|
|
75
75
|
statusline: true,
|
|
76
|
-
}
|
|
76
|
+
},
|
|
77
77
|
// Standards: UNVERIFIED — see slice #012+ (Cursor real-install dogfood for
|
|
78
78
|
// the `standardsProfile` and `skillInstall` fields). Until then, `peaks
|
|
79
79
|
// standards init` on a Cursor-detected project falls back to the Claude
|
|
@@ -81,4 +81,24 @@ export const CURSOR_ADAPTER = {
|
|
|
81
81
|
// postinstall script writes skills + output-styles to the legacy
|
|
82
82
|
// `~/.claude/{skills,output-styles}` paths with a stderr warning. Users
|
|
83
83
|
// who want Cursor-specific paths must move the files manually.
|
|
84
|
+
// Slice 4.0.8 RD §5: Cursor vendor signal is unverified; reserve-only resolver.
|
|
85
|
+
resolveCallerId: (env) => {
|
|
86
|
+
const e = env ?? process.env;
|
|
87
|
+
const override = e.PEAKS_CALLER_ID;
|
|
88
|
+
if (typeof override === 'string' && override.trim().length > 0) {
|
|
89
|
+
const trimmed = override.trim();
|
|
90
|
+
if (/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed))
|
|
91
|
+
return trimmed;
|
|
92
|
+
}
|
|
93
|
+
// Reserved vendor variable (UNVERIFIED): do not synthesize project identity.
|
|
94
|
+
const candidate = e.CURSOR_SESSION_ID;
|
|
95
|
+
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
96
|
+
const trimmed = candidate.trim();
|
|
97
|
+
if (/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed))
|
|
98
|
+
return trimmed;
|
|
99
|
+
}
|
|
100
|
+
const err = new Error('PEAKS_CALLER_NOT_RESOLVED: Cursor vendor signal unverified in 4.0.8');
|
|
101
|
+
err.code = 'PEAKS_CALLER_NOT_RESOLVED';
|
|
102
|
+
throw err;
|
|
103
|
+
},
|
|
84
104
|
};
|
|
@@ -47,5 +47,24 @@ export const HERMES_ADAPTER = {
|
|
|
47
47
|
capabilities: {
|
|
48
48
|
gateEnforce: true,
|
|
49
49
|
statusline: true
|
|
50
|
-
}
|
|
50
|
+
},
|
|
51
|
+
// Slice 4.0.8 RD §5: Hermes vendor signal unverified — fail closed.
|
|
52
|
+
resolveCallerId: (env) => {
|
|
53
|
+
const e = env ?? process.env;
|
|
54
|
+
const override = e.PEAKS_CALLER_ID;
|
|
55
|
+
if (typeof override === 'string' && override.trim().length > 0) {
|
|
56
|
+
const trimmed = override.trim();
|
|
57
|
+
if (/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed))
|
|
58
|
+
return trimmed;
|
|
59
|
+
}
|
|
60
|
+
const candidate = e.HERMES_SESSION_ID;
|
|
61
|
+
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
62
|
+
const trimmed = candidate.trim();
|
|
63
|
+
if (/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed))
|
|
64
|
+
return trimmed;
|
|
65
|
+
}
|
|
66
|
+
const err = new Error('PEAKS_CALLER_NOT_RESOLVED: Hermes vendor signal unverified');
|
|
67
|
+
err.code = 'PEAKS_CALLER_NOT_RESOLVED';
|
|
68
|
+
throw err;
|
|
69
|
+
},
|
|
51
70
|
};
|
|
@@ -38,5 +38,24 @@ export const OPENCLAW_ADAPTER = {
|
|
|
38
38
|
capabilities: {
|
|
39
39
|
gateEnforce: true,
|
|
40
40
|
statusline: true
|
|
41
|
-
}
|
|
41
|
+
},
|
|
42
|
+
// Slice 4.0.8 RD §5: OpenClaw vendor signal unverified — fail closed.
|
|
43
|
+
resolveCallerId: (env) => {
|
|
44
|
+
const e = env ?? process.env;
|
|
45
|
+
const override = e.PEAKS_CALLER_ID;
|
|
46
|
+
if (typeof override === 'string' && override.trim().length > 0) {
|
|
47
|
+
const trimmed = override.trim();
|
|
48
|
+
if (/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed))
|
|
49
|
+
return trimmed;
|
|
50
|
+
}
|
|
51
|
+
const candidate = e.OPENCLAW_SESSION_ID;
|
|
52
|
+
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
53
|
+
const trimmed = candidate.trim();
|
|
54
|
+
if (/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed))
|
|
55
|
+
return trimmed;
|
|
56
|
+
}
|
|
57
|
+
const err = new Error('PEAKS_CALLER_NOT_RESOLVED: OpenClaw vendor signal unverified');
|
|
58
|
+
err.code = 'PEAKS_CALLER_NOT_RESOLVED';
|
|
59
|
+
throw err;
|
|
60
|
+
},
|
|
42
61
|
};
|
|
@@ -53,5 +53,24 @@ export const QODER_ADAPTER = {
|
|
|
53
53
|
capabilities: {
|
|
54
54
|
gateEnforce: true,
|
|
55
55
|
statusline: true
|
|
56
|
-
}
|
|
56
|
+
},
|
|
57
|
+
// Slice 4.0.8 RD §5: Qoder vendor signal unverified — fail closed.
|
|
58
|
+
resolveCallerId: (env) => {
|
|
59
|
+
const e = env ?? process.env;
|
|
60
|
+
const override = e.PEAKS_CALLER_ID;
|
|
61
|
+
if (typeof override === 'string' && override.trim().length > 0) {
|
|
62
|
+
const trimmed = override.trim();
|
|
63
|
+
if (/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed))
|
|
64
|
+
return trimmed;
|
|
65
|
+
}
|
|
66
|
+
const candidate = e.QODER_SESSION_ID;
|
|
67
|
+
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
68
|
+
const trimmed = candidate.trim();
|
|
69
|
+
if (/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed))
|
|
70
|
+
return trimmed;
|
|
71
|
+
}
|
|
72
|
+
const err = new Error('PEAKS_CALLER_NOT_RESOLVED: Qoder vendor signal unverified');
|
|
73
|
+
err.code = 'PEAKS_CALLER_NOT_RESOLVED';
|
|
74
|
+
throw err;
|
|
75
|
+
},
|
|
57
76
|
};
|
|
@@ -41,5 +41,24 @@ export const TONGYI_LINGMA_ADAPTER = {
|
|
|
41
41
|
capabilities: {
|
|
42
42
|
gateEnforce: true,
|
|
43
43
|
statusline: true
|
|
44
|
-
}
|
|
44
|
+
},
|
|
45
|
+
// Slice 4.0.8 RD §5: Tongyi Lingma vendor signal unverified — fail closed.
|
|
46
|
+
resolveCallerId: (env) => {
|
|
47
|
+
const e = env ?? process.env;
|
|
48
|
+
const override = e.PEAKS_CALLER_ID;
|
|
49
|
+
if (typeof override === 'string' && override.trim().length > 0) {
|
|
50
|
+
const trimmed = override.trim();
|
|
51
|
+
if (/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed))
|
|
52
|
+
return trimmed;
|
|
53
|
+
}
|
|
54
|
+
const candidate = e.TONGYI_LINGMA_SESSION_ID;
|
|
55
|
+
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
56
|
+
const trimmed = candidate.trim();
|
|
57
|
+
if (/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed))
|
|
58
|
+
return trimmed;
|
|
59
|
+
}
|
|
60
|
+
const err = new Error('PEAKS_CALLER_NOT_RESOLVED: Tongyi Lingma vendor signal unverified');
|
|
61
|
+
err.code = 'PEAKS_CALLER_NOT_RESOLVED';
|
|
62
|
+
throw err;
|
|
63
|
+
},
|
|
45
64
|
};
|
|
@@ -71,7 +71,7 @@ export const TRAE_ADAPTER = {
|
|
|
71
71
|
capabilities: {
|
|
72
72
|
gateEnforce: true,
|
|
73
73
|
statusline: true,
|
|
74
|
-
}
|
|
74
|
+
},
|
|
75
75
|
// Standards: UNVERIFIED — see slice #012+ (Trae real-install dogfood for
|
|
76
76
|
// the `standardsProfile` and `skillInstall` fields). The slice #011
|
|
77
77
|
// framework lands; per-IDE values for Trae are a follow-up gated on
|
|
@@ -80,4 +80,24 @@ export const TRAE_ADAPTER = {
|
|
|
80
80
|
// (CLAUDE.md + .claude/rules/**) with a stderr warning, and the
|
|
81
81
|
// postinstall script writes skills + output-styles to the legacy
|
|
82
82
|
// `~/.claude/{skills,output-styles}` paths with a stderr warning.
|
|
83
|
+
// Slice 4.0.8 RD §5: Trae resolves PEAKS_CALLER_ID (override) →
|
|
84
|
+
// TRAE_SESSION_ID. Empty/invalid → typed PEAKS_CALLER_NOT_RESOLVED.
|
|
85
|
+
resolveCallerId: (env) => {
|
|
86
|
+
const e = env ?? process.env;
|
|
87
|
+
const override = e.PEAKS_CALLER_ID;
|
|
88
|
+
if (typeof override === 'string' && override.trim().length > 0) {
|
|
89
|
+
const trimmed = override.trim();
|
|
90
|
+
if (/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed))
|
|
91
|
+
return trimmed;
|
|
92
|
+
}
|
|
93
|
+
const v = e.TRAE_SESSION_ID;
|
|
94
|
+
if (typeof v === 'string' && v.trim().length > 0) {
|
|
95
|
+
const trimmed = v.trim();
|
|
96
|
+
if (/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed))
|
|
97
|
+
return trimmed;
|
|
98
|
+
}
|
|
99
|
+
const err = new Error('PEAKS_CALLER_NOT_RESOLVED: no Trae session id available');
|
|
100
|
+
err.code = 'PEAKS_CALLER_NOT_RESOLVED';
|
|
101
|
+
throw err;
|
|
102
|
+
},
|
|
83
103
|
};
|
|
@@ -217,4 +217,23 @@ export const ZCODE_ADAPTER = {
|
|
|
217
217
|
// provider → first enabled → first provider). Returns undefined
|
|
218
218
|
// on any failure (consistent with the cross-IDE contract).
|
|
219
219
|
detectCurrentModel: detectZcodeCurrentModel,
|
|
220
|
+
// Slice 4.0.8 RD §5: z-code vendor signal unverified — fail closed.
|
|
221
|
+
resolveCallerId: (env) => {
|
|
222
|
+
const e = env ?? process.env;
|
|
223
|
+
const override = e.PEAKS_CALLER_ID;
|
|
224
|
+
if (typeof override === 'string' && override.trim().length > 0) {
|
|
225
|
+
const trimmed = override.trim();
|
|
226
|
+
if (/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed))
|
|
227
|
+
return trimmed;
|
|
228
|
+
}
|
|
229
|
+
const candidate = e.ZCODE_SESSION_ID;
|
|
230
|
+
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
231
|
+
const trimmed = candidate.trim();
|
|
232
|
+
if (/^[a-zA-Z0-9._-]{1,200}$/.test(trimmed))
|
|
233
|
+
return trimmed;
|
|
234
|
+
}
|
|
235
|
+
const err = new Error('PEAKS_CALLER_NOT_RESOLVED: z-code vendor signal unverified');
|
|
236
|
+
err.code = 'PEAKS_CALLER_NOT_RESOLVED';
|
|
237
|
+
throw err;
|
|
238
|
+
},
|
|
220
239
|
};
|