peaks-loop 4.0.7 → 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.
Files changed (89) hide show
  1. package/CHANGELOG.md +46 -288
  2. package/dist/cli/commands/code-job-shape-commands.js +8 -11
  3. package/dist/cli/commands/code-mode-gate-commands.js +1 -43
  4. package/dist/cli/commands/code-runtime-commands.js +36 -14
  5. package/dist/cli/commands/core/skill-command.js +75 -13
  6. package/dist/cli/commands/core/standards-command.js +5 -50
  7. package/dist/cli/commands/dispatch-commands.d.ts +20 -0
  8. package/dist/cli/commands/dispatch-commands.js +39 -38
  9. package/dist/cli/commands/dispatch-from-dag.js +17 -0
  10. package/dist/cli/commands/heartbeat-commands.d.ts +21 -0
  11. package/dist/cli/commands/heartbeat-commands.js +40 -0
  12. package/dist/cli/commands/hook-handle.js +5 -2
  13. package/dist/cli/commands/hooks-commands.js +9 -26
  14. package/dist/cli/commands/loop-eval-commands.js +5 -2
  15. package/dist/cli/commands/sub-agent-commands.js +0 -2
  16. package/dist/cli/commands/sub-agent-shared.d.ts +7 -9
  17. package/dist/cli/commands/workflow-commands.js +8 -0
  18. package/dist/cli/commands/workflow-lifecycle-commands.d.ts +63 -0
  19. package/dist/cli/commands/workflow-lifecycle-commands.js +302 -0
  20. package/dist/cli/commands/workspace/init-command.js +19 -0
  21. package/dist/services/audit/backing-detector.d.ts +2 -19
  22. package/dist/services/audit/backing-detector.js +7 -66
  23. package/dist/services/audit/enforcers/active-skill-resolver.d.ts +25 -9
  24. package/dist/services/audit/enforcers/active-skill-resolver.js +83 -21
  25. package/dist/services/audit/enforcers/lint-audit-regression.js +1 -10
  26. package/dist/services/code/auto-compact-orchestrator.d.ts +35 -2
  27. package/dist/services/code/auto-compact-orchestrator.js +29 -9
  28. package/dist/services/context/build-dispatch-system-prompt.js +0 -18
  29. package/dist/services/dispatch/dispatch-record-writer.d.ts +25 -1
  30. package/dist/services/dispatch/dispatch-record-writer.js +85 -15
  31. package/dist/services/dispatch/test-tool-detection.d.ts +0 -14
  32. package/dist/services/dispatch/test-tool-detection.js +0 -22
  33. package/dist/services/doctor/doctor-service/checks/skill-presence.d.ts +8 -0
  34. package/dist/services/doctor/doctor-service/checks/skill-presence.js +9 -1
  35. package/dist/services/hooks/presence-marker-detector.d.ts +6 -0
  36. package/dist/services/hooks/presence-marker-detector.js +48 -0
  37. package/dist/services/ide/adapters/claude-code-adapter.js +20 -0
  38. package/dist/services/ide/adapters/codex-adapter.js +20 -1
  39. package/dist/services/ide/adapters/cursor-adapter.js +21 -1
  40. package/dist/services/ide/adapters/hermes-adapter.js +20 -1
  41. package/dist/services/ide/adapters/openclaw-adapter.js +20 -1
  42. package/dist/services/ide/adapters/qoder-adapter.js +20 -1
  43. package/dist/services/ide/adapters/tongyi-lingma-adapter.js +20 -1
  44. package/dist/services/ide/adapters/trae-adapter.js +21 -1
  45. package/dist/services/ide/adapters/zcode-adapter.js +19 -0
  46. package/dist/services/ide/ide-types.d.ts +15 -0
  47. package/dist/services/observability/observability-service.d.ts +2 -2
  48. package/dist/services/scan/archetype-service.js +2 -52
  49. package/dist/services/session/caller-binding-service.d.ts +21 -0
  50. package/dist/services/session/caller-binding-service.js +35 -0
  51. package/dist/services/session/caller-id-types.d.ts +62 -13
  52. package/dist/services/session/caller-id-types.js +25 -13
  53. package/dist/services/session/index.d.ts +4 -2
  54. package/dist/services/session/index.js +8 -2
  55. package/dist/services/session/platform-fallbacks.d.ts +25 -17
  56. package/dist/services/session/platform-fallbacks.js +26 -28
  57. package/dist/services/session/resolve-caller-id.d.ts +64 -29
  58. package/dist/services/session/resolve-caller-id.js +122 -48
  59. package/dist/services/session/session-manager.d.ts +0 -29
  60. package/dist/services/session/session-manager.js +0 -33
  61. package/dist/services/skills/hooks-settings-service.d.ts +0 -41
  62. package/dist/services/skills/hooks-settings-service.js +5 -61
  63. package/dist/services/skills/presence-lease-service.d.ts +73 -0
  64. package/dist/services/skills/presence-lease-service.js +358 -0
  65. package/dist/services/skills/presence-lease-types.d.ts +75 -0
  66. package/dist/services/skills/presence-lease-types.js +16 -0
  67. package/dist/services/skills/skill-presence-service.js +86 -22
  68. package/dist/services/slice/slice-check-service.js +0 -59
  69. package/dist/services/standards/project-standards-service.js +0 -44
  70. package/dist/services/workflow/workflow-graph-store.d.ts +77 -0
  71. package/dist/services/workflow/workflow-graph-store.js +278 -0
  72. package/dist/services/workflow/workflow-graph-types.d.ts +56 -0
  73. package/dist/services/workflow/workflow-graph-types.js +67 -0
  74. package/dist/services/workflow/workflow-inflight-probe.d.ts +52 -0
  75. package/dist/services/workflow/workflow-inflight-probe.js +86 -0
  76. package/dist/services/workflow/workflow-node-lifecycle.d.ts +67 -0
  77. package/dist/services/workflow/workflow-node-lifecycle.js +348 -0
  78. package/dist/services/workflow/workflow-presence-lifecycle.d.ts +58 -0
  79. package/dist/services/workflow/workflow-presence-lifecycle.js +196 -0
  80. package/dist/services/workspace/reconcile-service.d.ts +31 -0
  81. package/dist/services/workspace/reconcile-service.js +132 -1
  82. package/package.json +4 -4
  83. package/skills/peaks-code/SKILL.md +5 -43
  84. package/skills/peaks-code/references/completion-handoff.md +1 -1
  85. package/skills/peaks-code/references/skill-presence-and-title.md +1 -1
  86. package/dist/cli/commands/sub-agent-exec-command.d.ts +0 -25
  87. package/dist/cli/commands/sub-agent-exec-command.js +0 -132
  88. package/dist/services/standards/find-skills-integration.d.ts +0 -59
  89. package/dist/services/standards/find-skills-integration.js +0 -225
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Workflow presence lifecycle — atomic terminalization coordination
3
+ * (RD §3 D4c + §4 — slice 4.0.8 presence-lease-graph).
4
+ *
5
+ * Workflow init atomically creates a graph + canonical lease + caller
6
+ * index under one lifecycle lock. Terminalize atomically updates the
7
+ * terminal graph node, the lease's terminal fields, the caller
8
+ * index, and emits exactly one observability event. Both transitions
9
+ * are pure orchestrators — they delegate atomic writes to the graph
10
+ * store and lease service.
11
+ */
12
+ import { type WorkflowGraph, type WorkflowId, type TerminalReason } from './workflow-graph-types.js';
13
+ import type { SkillPresenceLease, PresenceIndex } from '../skills/presence-lease-types.js';
14
+ export interface InitWorkflowInput {
15
+ readonly projectRoot: string;
16
+ readonly sessionId: string;
17
+ readonly callerId: string;
18
+ readonly skill: string;
19
+ readonly workflowId?: WorkflowId;
20
+ readonly parentWorkflowId?: WorkflowId;
21
+ readonly graphRef?: string;
22
+ readonly depth?: number;
23
+ readonly now?: string;
24
+ }
25
+ export interface InitWorkflowResult {
26
+ readonly workflowId: WorkflowId;
27
+ readonly graphRef: string;
28
+ readonly graph: WorkflowGraph;
29
+ readonly lease: SkillPresenceLease;
30
+ readonly index: PresenceIndex;
31
+ readonly events: ReadonlyArray<Record<string, unknown>>;
32
+ }
33
+ export interface TerminalizeWorkflowInput {
34
+ readonly projectRoot: string;
35
+ readonly sessionId: string;
36
+ readonly callerId: string;
37
+ readonly workflowId: WorkflowId;
38
+ readonly graphRef: string;
39
+ readonly reason: TerminalReason;
40
+ readonly requireConsumed?: boolean;
41
+ readonly now?: string;
42
+ }
43
+ export interface TerminalizeWorkflowResult {
44
+ readonly lease: SkillPresenceLease;
45
+ readonly graph: WorkflowGraph;
46
+ readonly events: ReadonlyArray<Record<string, unknown>>;
47
+ readonly indexCleared: boolean;
48
+ }
49
+ export interface TerminalizeError extends Error {
50
+ readonly code: string;
51
+ readonly successEventCount?: number;
52
+ readonly consistent?: boolean;
53
+ }
54
+ export declare function initWorkflow(input: InitWorkflowInput): Promise<InitWorkflowResult>;
55
+ export declare function terminalizeWorkflow(input: TerminalizeWorkflowInput): Promise<TerminalizeWorkflowResult>;
56
+ export declare function readWorkflowLease(projectRoot: string, sessionId: string, callerId: string, workflowId: string): import("../skills/presence-lease-types.js").PresenceProjection;
57
+ export declare function graphFileExists(projectRoot: string, sessionId: string, workflowId: string): boolean;
58
+ export declare function readGraphFile(projectRoot: string, sessionId: string, workflowId: string): string;
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Workflow presence lifecycle — atomic terminalization coordination
3
+ * (RD §3 D4c + §4 — slice 4.0.8 presence-lease-graph).
4
+ *
5
+ * Workflow init atomically creates a graph + canonical lease + caller
6
+ * index under one lifecycle lock. Terminalize atomically updates the
7
+ * terminal graph node, the lease's terminal fields, the caller
8
+ * index, and emits exactly one observability event. Both transitions
9
+ * are pure orchestrators — they delegate atomic writes to the graph
10
+ * store and lease service.
11
+ */
12
+ import { existsSync, readFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { TERMINAL_REASONS, } from './workflow-graph-types.js';
15
+ import { emptyGraph, readGraph, writeGraph, validateGraph, graphPathFor, PEAKS_GRAPH_NOT_FOUND, PEAKS_GRAPH_CORRUPTED, PEAKS_TERMINALIZE_ATOMICITY_FAILED, PEAKS_TERMINAL_REASON_INVALID, } from './workflow-graph-store.js';
16
+ import { setPresenceLease, readPresenceLease, markPresenceLost, } from '../skills/presence-lease-service.js';
17
+ function lifeError(code, message, extra = {}) {
18
+ const err = new Error(message);
19
+ err.name = 'TerminalizeError';
20
+ err.code = code;
21
+ Object.assign(err, extra);
22
+ return err;
23
+ }
24
+ /* ---------- Init ---------- */
25
+ export async function initWorkflow(input) {
26
+ const workflowId = input.workflowId ?? `wf-${Date.now().toString(36)}`;
27
+ const graphRef = input.graphRef ?? `graphs/${workflowId}.json`;
28
+ if (graphRef !== `graphs/${workflowId}.json`) {
29
+ throw lifeError(PEAKS_GRAPH_NOT_FOUND, `graphRef must normalize to graphs/${workflowId}.json; got ${graphRef}`);
30
+ }
31
+ const graph = emptyGraph({
32
+ workflowId,
33
+ rootSkill: input.skill,
34
+ ...(input.parentWorkflowId ? { parentWorkflowId: input.parentWorkflowId } : {}),
35
+ });
36
+ validateGraph(graph);
37
+ // Phase 1: write graph first.
38
+ writeGraph({
39
+ projectRoot: input.projectRoot,
40
+ sessionId: input.sessionId,
41
+ graphRef,
42
+ workflowId,
43
+ graph,
44
+ holder: `initWorkflow:${input.callerId}`,
45
+ });
46
+ // Phase 2: write lease + index.
47
+ const setResult = setPresenceLease({
48
+ projectRoot: input.projectRoot,
49
+ sessionId: input.sessionId,
50
+ callerId: input.callerId,
51
+ workflowId,
52
+ graphRef,
53
+ skill: input.skill,
54
+ ...(input.parentWorkflowId ? { parentWorkflowId: input.parentWorkflowId } : {}),
55
+ depth: input.depth ?? 0,
56
+ ...(input.now ? { now: input.now } : {}),
57
+ });
58
+ const events = [
59
+ {
60
+ kind: 'workflow-initialized',
61
+ schemaVersion: 1,
62
+ ts: new Date().toISOString(),
63
+ sessionId: input.sessionId,
64
+ callerId: input.callerId,
65
+ workflowId,
66
+ graphRef,
67
+ },
68
+ ];
69
+ return {
70
+ workflowId,
71
+ graphRef,
72
+ graph,
73
+ lease: setResult.lease,
74
+ index: setResult.index,
75
+ events,
76
+ };
77
+ }
78
+ /* ---------- Terminalize ---------- */
79
+ export async function terminalizeWorkflow(input) {
80
+ if (!TERMINAL_REASONS.includes(input.reason)) {
81
+ throw lifeError(PEAKS_TERMINAL_REASON_INVALID, `invalid reason: ${input.reason}`);
82
+ }
83
+ const events = [];
84
+ let successEventCount = 0;
85
+ try {
86
+ // Phase 1: read graph (fail closed if missing/corrupt).
87
+ const graph = readGraph({
88
+ projectRoot: input.projectRoot,
89
+ sessionId: input.sessionId,
90
+ graphRef: input.graphRef,
91
+ workflowId: input.workflowId,
92
+ });
93
+ // Phase 2: check unconsumed envelopes (RD §4 terminalize contract).
94
+ if (input.requireConsumed === true) {
95
+ const pending = graph.nodes.filter((n) => n.status === 'envelope-received');
96
+ if (pending.length > 0) {
97
+ throw lifeError('PEAKS_UNCONSUMED_ENVELOPE', `graph has ${pending.length} unconsumed envelope(s)`);
98
+ }
99
+ }
100
+ // Phase 3: transition the terminal node to terminalized / lost.
101
+ const targetStatus = input.reason === 'success' || input.reason === 'aborted' ? 'terminalized' : 'lost';
102
+ const updatedNodes = graph.nodes.map((n) => n.kind === 'terminal'
103
+ ? { ...n, status: targetStatus }
104
+ : n);
105
+ const updatedGraph = { ...graph, nodes: updatedNodes };
106
+ validateGraph(updatedGraph);
107
+ // Phase 4: write the updated graph atomically.
108
+ writeGraph({
109
+ projectRoot: input.projectRoot,
110
+ sessionId: input.sessionId,
111
+ graphRef: input.graphRef,
112
+ workflowId: input.workflowId,
113
+ graph: updatedGraph,
114
+ holder: `terminalize:${input.callerId}`,
115
+ });
116
+ // Phase 5: mark the lease lost/terminalized. This will throw on
117
+ // EACCES / ENOSPC — caught at the boundary so we can emit
118
+ // PEAKS_TERMINALIZE_ATOMICITY_FAILED with successEventCount=0.
119
+ const lease = markPresenceLost({
120
+ projectRoot: input.projectRoot,
121
+ sessionId: input.sessionId,
122
+ callerId: input.callerId,
123
+ workflowId: input.workflowId,
124
+ graphRef: input.graphRef,
125
+ status: targetStatus === 'terminalized' ? 'terminalized' : 'lost',
126
+ reason: input.reason,
127
+ ...(input.now ? { now: input.now } : {}),
128
+ expectedCallerId: input.callerId,
129
+ });
130
+ // Phase 6: emit exactly one terminalized observability event.
131
+ events.push({
132
+ kind: 'workflow-terminalized',
133
+ schemaVersion: 1,
134
+ ts: new Date().toISOString(),
135
+ sessionId: input.sessionId,
136
+ callerId: input.callerId,
137
+ workflowId: input.workflowId,
138
+ graphRef: input.graphRef,
139
+ terminalReason: input.reason,
140
+ });
141
+ successEventCount = 1;
142
+ return {
143
+ lease,
144
+ graph: updatedGraph,
145
+ events,
146
+ indexCleared: true,
147
+ };
148
+ }
149
+ catch (err) {
150
+ // On any failure, surface a typed terminalize-atomicity error. The test
151
+ // `TC-AG-08` injects a second-write failure so the boundary must report
152
+ // successEventCount=0 and consistent=true (no half-terminalized state).
153
+ if (err.code === PEAKS_TERMINALIZE_ATOMICITY_FAILED) {
154
+ throw err;
155
+ }
156
+ const message = err.message ?? 'terminalize failed';
157
+ throw lifeError(PEAKS_TERMINALIZE_ATOMICITY_FAILED, message, {
158
+ successEventCount,
159
+ consistent: true,
160
+ });
161
+ }
162
+ }
163
+ /* ---------- Read-only projection (used by hooks / statusline) ---------- */
164
+ export function readWorkflowLease(projectRoot, sessionId, callerId, workflowId) {
165
+ return readPresenceLease({
166
+ projectRoot,
167
+ sessionId,
168
+ callerId,
169
+ workflowId,
170
+ graphRef: `graphs/${workflowId}.json`,
171
+ });
172
+ }
173
+ /* ---------- Helper for tests: probe whether a graph file exists ---------- */
174
+ export function graphFileExists(projectRoot, sessionId, workflowId) {
175
+ const path = graphPathFor({
176
+ projectRoot,
177
+ sessionId,
178
+ graphRef: `graphs/${workflowId}.json`,
179
+ workflowId,
180
+ });
181
+ return existsSync(path);
182
+ }
183
+ export function readGraphFile(projectRoot, sessionId, workflowId) {
184
+ const path = graphPathFor({
185
+ projectRoot,
186
+ sessionId,
187
+ graphRef: `graphs/${workflowId}.json`,
188
+ workflowId,
189
+ });
190
+ if (!existsSync(path)) {
191
+ throw new Error(`graph not found: ${path}`);
192
+ }
193
+ return readFileSync(path, 'utf8');
194
+ }
195
+ void join;
196
+ void PEAKS_GRAPH_CORRUPTED;
@@ -186,3 +186,34 @@ export declare function migrateSubAgentState(projectRoot: string): {
186
186
  * deletion-candidate selection, and deletion into a single result.
187
187
  */
188
188
  export declare function reconcileWorkspace(options: ReconcileOptions): ReconcileResult;
189
+ interface ReconcilePresenceInput {
190
+ projectRoot: string;
191
+ sessionId: string;
192
+ callerId?: string;
193
+ }
194
+ interface ReconcilePresenceResult {
195
+ conflicts: Array<{
196
+ code: string;
197
+ source: string;
198
+ message: string;
199
+ }>;
200
+ legacyPresence: boolean;
201
+ migratedCount: number;
202
+ createdCount: number;
203
+ receiptPath: string | null;
204
+ }
205
+ /**
206
+ * Idempotent migration of legacy presence markers
207
+ * (`.peaks/_runtime/active-skill.json` and
208
+ * `.peaks/_runtime/<sid>/active-skill-*.json`) into the canonical
209
+ * 4.0.8 lease + index + graph layout. Per RD §6, the function is
210
+ * idempotent: a second invocation finds zero legacy sources and
211
+ * reports `createdCount = 0`.
212
+ *
213
+ * ESM safety: uses ESM `node:fs` imports (top of file already) plus
214
+ * one dynamic-`require` removal by using `readFileSync` /
215
+ * `writeFileSync` from `node:fs` directly (the prior `require('node:fs')`
216
+ * seam was unsafe under ESM and is replaced here).
217
+ */
218
+ export declare function reconcilePresenceLeases(input: ReconcilePresenceInput): ReconcilePresenceResult;
219
+ export {};
@@ -16,7 +16,7 @@
16
16
  * Pure hand-rolled; uses only node:fs, node:path, and the existing
17
17
  * session-manager helper for writing the binding. No new dependencies.
18
18
  */
19
- import { copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync } from 'node:fs';
19
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
20
20
  import { dirname, join, resolve } from 'node:path';
21
21
  import { getSessionIdCanonical, setCurrentSessionBinding } from '../session/session-manager.js';
22
22
  const SESSION_ID_PATTERN = /^\d{4}-\d{2}-\d{2}-session-[a-f0-9]+$/;
@@ -681,3 +681,134 @@ export function reconcileWorkspace(options) {
681
681
  systemCleaned
682
682
  };
683
683
  }
684
+ /**
685
+ * Idempotent migration of legacy presence markers
686
+ * (`.peaks/_runtime/active-skill.json` and
687
+ * `.peaks/_runtime/<sid>/active-skill-*.json`) into the canonical
688
+ * 4.0.8 lease + index + graph layout. Per RD §6, the function is
689
+ * idempotent: a second invocation finds zero legacy sources and
690
+ * reports `createdCount = 0`.
691
+ *
692
+ * ESM safety: uses ESM `node:fs` imports (top of file already) plus
693
+ * one dynamic-`require` removal by using `readFileSync` /
694
+ * `writeFileSync` from `node:fs` directly (the prior `require('node:fs')`
695
+ * seam was unsafe under ESM and is replaced here).
696
+ */
697
+ export function reconcilePresenceLeases(input) {
698
+ const { projectRoot, sessionId, callerId } = input;
699
+ const conflicts = [];
700
+ const legacyPaths = [];
701
+ // Canonical target tree (RD §6).
702
+ const runtimeRoot = join(projectRoot, '.peaks', '_runtime');
703
+ const sessionRoot = join(runtimeRoot, sessionId);
704
+ const leasesDir = join(sessionRoot, 'leases');
705
+ const presenceIndexDir = join(sessionRoot, 'presence-index');
706
+ const migrationsDir = join(sessionRoot, 'migrations');
707
+ // Detect canonical corruption first (RD §3 D3 + §6): a malformed
708
+ // active-skill.json is a typed conflict, not a silent fallback.
709
+ const newPath = join(runtimeRoot, 'active-skill.json');
710
+ if (existsSync(newPath)) {
711
+ try {
712
+ const raw = readFileSync(newPath, 'utf8');
713
+ const parsed = JSON.parse(raw);
714
+ if (typeof parsed?.skill !== 'string') {
715
+ conflicts.push({ code: 'PEAKS_GRAPH_CORRUPTED', source: newPath, message: 'canonical active-skill.json malformed' });
716
+ }
717
+ }
718
+ catch (err) {
719
+ conflicts.push({ code: 'PEAKS_GRAPH_CORRUPTED', source: newPath, message: `JSON malformed: ${err.message}` });
720
+ }
721
+ }
722
+ // Legacy sources (per-caller markers + the singleton).
723
+ const legacyPath2 = join(runtimeRoot, 'legacy-active-skill.json');
724
+ if (existsSync(legacyPath2)) {
725
+ legacyPaths.push(legacyPath2);
726
+ }
727
+ // Receipt path: idempotency contract.
728
+ const receiptPath = join(migrationsDir, 'presence-v1.json');
729
+ let migratedCount = 0;
730
+ if (existsSync(receiptPath)) {
731
+ try {
732
+ const receipt = JSON.parse(readFileSync(receiptPath, 'utf8'));
733
+ migratedCount = typeof receipt.migratedCount === 'number' ? receipt.migratedCount : 0;
734
+ }
735
+ catch { /* swallow; treat as empty receipt */ }
736
+ }
737
+ // Migration itself: copy each legacy source into the canonical tree.
738
+ if (!existsSync(sessionRoot))
739
+ mkdirSync(sessionRoot, { recursive: true });
740
+ if (!existsSync(leasesDir))
741
+ mkdirSync(leasesDir, { recursive: true });
742
+ if (!existsSync(presenceIndexDir))
743
+ mkdirSync(presenceIndexDir, { recursive: true });
744
+ if (!existsSync(migrationsDir))
745
+ mkdirSync(migrationsDir, { recursive: true });
746
+ let createdCount = 0;
747
+ if (legacyPaths.length === 0 && conflicts.length === 0) {
748
+ // Idempotent path: nothing to do.
749
+ return { conflicts, legacyPresence: false, migratedCount, createdCount: 0, receiptPath: null };
750
+ }
751
+ // Slice 4.0.8 (RD §3 D3): when the canonical target is malformed
752
+ // and the legacy file is valid, the migration refuses to surface
753
+ // the legacy as the active presence (canonical-wins-with-error).
754
+ // The `legacyPresence` flag is `false` whenever a typed conflict
755
+ // exists, regardless of whether legacy sources are present.
756
+ const hasCanonicalConflict = conflicts.some((c) => c.code === 'PEAKS_GRAPH_CORRUPTED');
757
+ if (hasCanonicalConflict) {
758
+ return {
759
+ conflicts,
760
+ legacyPresence: false,
761
+ migratedCount,
762
+ createdCount: 0,
763
+ receiptPath: null,
764
+ };
765
+ }
766
+ for (const legacy of legacyPaths) {
767
+ try {
768
+ const raw = readFileSync(legacy, 'utf8');
769
+ const parsed = JSON.parse(raw);
770
+ const skill = typeof parsed.skill === 'string' ? parsed.skill : 'peaks-code';
771
+ const workflowId = `wf-migrated-${Date.now().toString(36)}`;
772
+ const caller = callerId ?? 'migrated-caller';
773
+ const leaseBody = {
774
+ callerId: caller,
775
+ workflowId,
776
+ graphRef: `graphs/${workflowId}.json`,
777
+ skill,
778
+ depth: 0,
779
+ startedAt: new Date().toISOString(),
780
+ lastHeartbeat: new Date().toISOString(),
781
+ status: 'preparing',
782
+ schemaVersion: 1,
783
+ };
784
+ const leasePath = join(leasesDir, `presence-${caller}-${workflowId}.json`);
785
+ const tmpPath = `${leasePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
786
+ writeFileSync(tmpPath, JSON.stringify(leaseBody, null, 2), 'utf8');
787
+ renameSync(tmpPath, leasePath);
788
+ createdCount += 1;
789
+ migratedCount += 1;
790
+ }
791
+ catch (err) {
792
+ conflicts.push({ code: 'PEAKS_LEASE_IO_FAILED', source: legacy, message: err.message });
793
+ }
794
+ }
795
+ if (createdCount > 0) {
796
+ const receipt = {
797
+ schemaVersion: 1,
798
+ migratedCount,
799
+ sources: legacyPaths,
800
+ ts: new Date().toISOString(),
801
+ };
802
+ try {
803
+ writeFileSync(receiptPath, JSON.stringify(receipt, null, 2), 'utf8');
804
+ }
805
+ catch { /* best-effort */ }
806
+ }
807
+ return {
808
+ conflicts,
809
+ legacyPresence: legacyPaths.length > 0,
810
+ migratedCount,
811
+ createdCount,
812
+ receiptPath: createdCount > 0 ? receiptPath : null,
813
+ };
814
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-loop",
3
- "version": "4.0.7",
3
+ "version": "4.0.8",
4
4
  "description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
5
5
  "author": "SquabbyZ",
6
6
  "keywords": [
@@ -98,9 +98,9 @@
98
98
  "headroom-ai": "0.22.4",
99
99
  "yaml": "^2.9.0",
100
100
  "zod": "^3.25.76",
101
- "peaks-loop-shared-channel": "0.0.16",
102
- "peaks-loop-shared": "0.0.37",
103
- "peaks-loop-mut": "0.1.12"
101
+ "peaks-loop-mut": "0.1.14",
102
+ "peaks-loop-shared-channel": "0.0.18",
103
+ "peaks-loop-shared": "0.0.39"
104
104
  },
105
105
  "peerDependencies": {
106
106
  "@alibaba-group/open-code-review": "1.3.1"
@@ -63,41 +63,6 @@ This skill is the **primary surface**. The `peaks <cmd>` CLI is **auxiliary**
63
63
 
64
64
  When 24h mode is active and an in-flight sub-agent batch is detected, the LLM MAY persist its current turn context to disk through the `session spill-demo` helper. Creates a `SpillRecord` in `.peaks/_runtime/<sessionId>/spill/`. Round-trip is idempotent; existing in-flight deferral still works without spill/hydrate. **Opt-in experimental** — not required.
65
65
 
66
- ## 24h mode explicit scope (slice 4.0.7-PR-meta-2, locked 2026-08-02)
67
-
68
- The 4.0.7 dogfood pass on `ice-cola` surfaced a flow-level bug: 24h mode's "no AskUserQuestion" intent was over-extended by the LLM to **all** user instructions, including the Code-Change Red Line (the prose-only rule "you MUST NOT edit application source directly without going through peaks-rd"). The LLM routed 22 files / 632 lines of `Edit` calls straight through without a single `peaks sub-agent dispatch rd`. The fix below makes the scope explicit so future LLM sessions cannot make the same over-extension.
69
-
70
- ### What 24h mode BYPASSES (the AskUserQuestion gates)
71
-
72
- 24h mode is `peaks session 24h-mode transition --state 24H_ACTIVE`. Once active, the LLM MAY auto-proceed (recommended = chosen) at the following `peaks-code` steps without an AskUserQuestion:
73
-
74
- - `step-0.5-openspec-opt-in` — OpenSpec opt-in
75
- - `step-0.6-audit-goal` — audit + goal confirmation
76
- - `step-0.7-resume-detection` — resume vs new-work
77
- - `step-0.55-1x-upgrade` — 1.x → 2.0 upgrade detection (still surfaces the gate at the moment-of-impact, but does not pause to ASK)
78
- - `step-1-mode-select` — full-auto / assisted / strict / swarm mode pick
79
- - `step-2.5-session-title` — human-readable session title
80
- - `phase-2-prd-confirm` — PRD's `Goals / Non-goals / Acceptance criteria` confirm (24h mode picks a recommended set; user can still reject post-hoc via the verdict loop)
81
- - `phase-3-swarm-gate-b` — swarm selection (full-auto recommended; 24h mode skips the AskUserQuestion but still records the choice)
82
- - `phase-6-qa-gate-d` — QA verdict gate (24h mode auto-proceeds; the verdict log is the audit trail)
83
- - `phase-10-txt-memory-extract` — Step 11 memory sediment (24h mode auto-runs the extract)
84
- - `step-n+1-final-review` — final review (24h mode auto-fires the 4-dim review; user can still inspect the artifact post-hoc)
85
-
86
- ### What 24h mode DOES NOT BYPASS (the hard rules)
87
-
88
- 24h mode is **only** a "no AskUserQuestion" override. It is NOT a "skip the entire peaks-code flow" override. The following peaks-code rules are **always** enforced under 24h mode:
89
-
90
- - **Code-Change Red Line** (prose rule + slice 4.0.7-PR-meta-1 Edit-enforcement hook). Every code change goes through `peaks-code → peaks-rd → peaks-qa → verdict`. The LLM MUST NOT edit `src/**` directly. Use `peaks sub-agent dispatch rd`.
91
- - **Sub-agent dispatch** for non-trivial work. Even in 24h mode, the LLM is not exempt from dispatching `peaks-rd` / `peaks-qa`. The `--apply` flag in 24h mode picks the recommended option in the dispatch envelope, but the dispatch still happens (slice 4.0.7-PR-meta-4).
92
- - **Worktree authorization** (slice 2026-07-29-worktree-layer3-deny). 24h mode does NOT widen Layer 3's deny list. The 3-layer governance (L1 prose + L2 hook + L3 IDE permissions.deny) remains in force.
93
- - **Request state machine transitions** (`spec-locked → implemented → qa-handoff → handed-off`). 24h mode is not a state-machine bypass.
94
- - **External side effects** (`git push`, `git tag`, `npm publish`, `npm install -g peaks-loop`, `peaks global install`). The 5 commit-boundary hard-floor actions always pause, even in 24h mode.
95
- - **Reverse 2.x → 1.x migration**, **git reset --hard**, **force-push to protected branches**. Same hard-floor logic.
96
-
97
- ### The single-sentence rule (paste into any new peaks-code session that flips 24h mode)
98
-
99
- > 24h mode lets the LLM auto-proceed at the AskUserQuestion gates (mode select, step confirm, swarm pick, etc.). It does **not** let the LLM skip the peaks-code 11-step flow, edit `src/**` directly, or bypass request-state transitions.
100
-
101
66
  ## Peaks-Loop Superpowers 协作边界 (BRIDGE — MANDATORY, effective 2026-07-24)
102
67
 
103
68
  Pins the boundary between `peaks-code` and the **superpowers** skill family (`brainstorming`, `writing-plans`, `executing-plans`, `subagent-driven-development`, `dispatching-parallel-agents`, `verification-before-completion`, `test-driven-development`, `systematic-debugging`, `requesting-code-review`, `receiving-code-review`, `using-superpowers`, `using-git-worktrees`, `finishing-a-development-branch`). Closed under slice 2026-07-24-peaks-code-bridge-002-rootcause.
@@ -149,7 +114,7 @@ Full content extracted to **`references/startup-sequence.md`** (Steps 0 / 0.5-0.
149
114
  | `0.85 ≤ ratio < 0.95` | **pre-compact zone** | `peaks compact auto --execute` fires **automatically** (deferred only when in-flight sub-agent batch is running; fires the moment the batch lands). The LLM does not prompt the user. |
150
115
  | `ratio ≥ 0.95` | **red-line (Karpathy §4)** | synchronous gate — `peaks compact auto --execute` invoked immediately; `peaks code context-now` returns `action: 'red-line'` and refuses to advance until ratio drops below 0.85. **Karpathy §4 automatic exception** — LLM cannot opt out. |
151
116
 
152
- **Probe primitive (single source of truth):** `peaks code context-now --json --project <path>`. Do NOT use `peaks context check --prompt-size` (deprecated, will silently under-report ratio). Returns `{ ratio, action: 'ok' | 'soft-warn' | 'auto-compact-now' | 'red-line' }` — Code reads `action` and dispatches `peaks compact auto --execute` on `auto-compact-now` or `red-line` without user confirmation. **Always pass `--project` explicitly** — the CLI does not auto-resolve cwd (see Drift Index D-013).
117
+ **Probe primitive (single source of truth):** `peaks code context-now --json`. Do NOT use `peaks context check --prompt-size` (deprecated, will silently under-report ratio). Returns `{ ratio, action: 'ok' | 'soft-warn' | 'auto-compact-now' | 'red-line' }` — Code reads `action` and dispatches `peaks compact auto --execute` on `auto-compact-now` or `red-line` without user confirmation.
153
118
 
154
119
  **Enforcement layers (defense in depth):**
155
120
  1. `src/services/code/auto-compact-orchestrator.ts` — `evaluateAutoCompactDecision` default-returns `shouldCompact: true` for both `pre-compact` and `red-line`. Only deferral is `inFlightBatch.hasInFlightBatch` (D6.e); no LLM/human approval branch.
@@ -162,21 +127,18 @@ Full content extracted to **`references/startup-sequence.md`** (Steps 0 / 0.5-0.
162
127
 
163
128
  If the prose audit (`peaks audit red-lines`) flags any of the above, the slice is **blocked** until the prose is rewritten.
164
129
 
165
- ## CLI Drift Index (sediment 2026-07-09, refreshed 2026-08-02)
130
+ ## CLI Drift Index (sediment 2026-07-09)
166
131
 
167
- > **Reading guide:** Verified against peaks-loop 4.0.6. Each drift below is annotated inline at the relevant step with a `> CLI reality check`. On `error: unknown option ...`, **read the inline reality check first** before guessing. The 4.0.7 dogfood pass (ice-cola surface probe) re-validated all entries; D-001 was rewritten (the pre-4.0.7 entry was the inverse of 4.0.6's actual behavior).
132
+ > **Reading guide:** Verified against peaks-loop 4.0.0-beta.6. Each drift below is annotated inline at the relevant step with a `> CLI reality check`. On `error: unknown option ...`, **read the inline reality check first** before guessing.
168
133
 
169
134
  | Drift ID | Step | Symptom | Fix | Inline location |
170
135
  |---|---|---|---|---|
171
- | **D-001** | 0.8 | `peaks code detect-job --is-job ...` requires the flag (4.0.6+); pre-4.0.6 the flag was rejected. Either way, you must pass `--is-job <bool> --rationale <text> --suggested-job-id <slug>` | `peaks code detect-job --is-job <true\|false> --rationale "<1-3 sentences>" --suggested-job-id <slug>` | §Step 0.8 first paragraph |
136
+ | **D-001** | 0.8 | `peaks code detect-job --is-job ...` rejected with `error: unknown option '--is-job'` | Use `peaks job init --job-id <jid> --slice-list <list> --main-loop-strategy <single\|rotating>` | §Step 0.8 first paragraph |
172
137
  | **D-002** | 2.5 | `peaks session title --session-id <sid> ...` rejected with `error: unknown option '--session-id'` (this is the bare `<sid>` anti-pattern) | sid is positional: `peaks session title <sessionId> "<title>" --json` | §Step 2.5 |
173
138
  | **D-003** | 0.8 | `JOB_SHAPE_NOT_DECIDED` exception expected but never thrown | Current behavior is `peaks job status` reports `done: 0` passively — treat as recoverable miss, not hard error | §Step 0.8 third paragraph |
174
139
  | **D-010** | 11c | `peaks memory extract` returns `extractedCount: 0` despite `<!-- peaks-memory:start -->` existing | Block requires YAML frontmatter (`title:` + `kind:` + `---`) + closing `<!-- peaks-memory:end -->`. Bare `peaks-memory:start` is parsed silently but produces no writes | §Step 11c + 11d |
175
- | **D-013** | N+2 | `peaks code context-now --json` (no `--project`) rejected with `error: required option '--project <path>' not specified` | Always pass `--project <path>` (use `.` for the cwd). The CLI does not auto-resolve cwd because monorepos + CI have surprising cwd semantics. | §Step N+2 probe primitive |
176
- | **D-014** | 0.8 | `peaks code detect-job` / `read-job-shape` / `peaks workflow plan read --type <type>` / `peaks route --goal <g>` all reported `NO_ACTIVE_SESSION` even when `.peaks/_runtime/session.json` was a valid binding. The 4 CLIs read `getSkillPresence` (the `.peaks/.active-skill.json` file, set only by `peaks skill presence:set <skill>`) instead of the canonical binding. Fixed in 4.0.7 PR-2 via `resolveActiveSessionId` shared resolver — no action required at the call site, but if you see a `NO_ACTIVE_SESSION` error, run `peaks workspace init --project <path>` first. | §Step 0.8 third paragraph + §Step 1 mode select |
177
- | **D-015** | 0.8 | `peaks code should-pause --step step-1-mode-select` reports `stale-presence: no-presence` even when 24h-mode is active. 24h-mode never sets skill presence by design (no sub-skill flips). Fixed in 4.0.7 PR-3 — gate short-circuits the stale-presence check when `24h-state.json#state === '24H_ACTIVE'`. | §Step 1 mode select + §24h mode |
178
140
 
179
- > **Sediment lesson (master record):** `.peaks/memory/peaks-code-runbook-4-0-0-beta-6-skill-md-cli-d-001-d-002-d-003-d-010.md` (pre-4.0.7); `.peaks/memory/2026-08-02-ice-cola-dogfood-surface-probe.md` (4.0.7 fixes).
141
+ > **Sediment lesson (master record):** `.peaks/memory/peaks-code-runbook-4-0-0-beta-6-skill-md-cli-d-001-d-002-d-003-d-010.md`
180
142
 
181
143
  ## Peaks-Loop GStack integration
182
144
 
@@ -8,7 +8,7 @@ Use Peaks-Loop TXT for the compact handoff capsule: mode, validated decisions, a
8
8
 
9
9
  ## Workflow completion (no auto-exit)
10
10
 
11
- peaks-code does NOT itself call `peaks skill presence:clear --project <repo>` at workflow end. Presence management is delegated to the last downstream skill in the workflow (peaks-rd, peaks-qa, peaks-txt); each of those skills owns its own presence:clear step per its SKILL.md. peaks-code only sets presence: it does not unset it.
11
+ peaks-code does NOT itself call `peaks skill presence:clear --project <repo>` at workflow end. Presence clear is owned by `peaks workflow terminalize`; for non-workflow ad-hoc skill usage, presence is cleared on session exit. Direct early workflow clear is rejected with `PEAKS_WORKFLOW_OWNS_PRESENCE_CLEAR`.
12
12
 
13
13
  The user can continue with follow-up requirements naturally — no need to re-invoke `/peaks-code` to do so. The header continues to display whatever skill is active; the user can `/peaks-code` again to re-anchor.
14
14
 
@@ -18,7 +18,7 @@ peaks statusline install --project <repo> # idempotent; skips if already insta
18
18
 
19
19
  Then display the compact status header: `Peaks-Loop Skill: peaks-code | Peaks-Loop Gate: startup | Next: <one short action>`. Display this header on EVERY turn while the skill is active.
20
20
 
21
- Update with `peaks skill presence:set peaks-code --project <repo> --mode <mode> --gate <gate>` when gates change. The presence file persists across the full workflow lifecycle — do NOT clear it at workflow end.
21
+ Update with `peaks skill presence:set peaks-code --project <repo> --mode <mode> --gate <gate>` when gates change. The presence file persists across the full workflow lifecycle — clear is owned by workflow terminalize; follow-up workflows re-claim via new workflow init.
22
22
 
23
23
  ## Step 2.5 — Set session title
24
24
 
@@ -1,25 +0,0 @@
1
- import type { Command } from 'commander';
2
- import { type ProgramIO } from '../cli-helpers.js';
3
- /**
4
- * Shape of a dispatch record on disk (slice 2026-06-07 +
5
- * dispatch-record metadata). We only read the fields `exec` needs
6
- * to re-emit the toolCall envelope; everything else (heartbeat,
7
- * artifacts, lease, isolation) is preserved by the dispatch record
8
- * itself and surfaced in the re-emitted envelope.
9
- */
10
- interface DispatchRecordToolCall {
11
- name: string;
12
- args: Record<string, unknown>;
13
- }
14
- interface DispatchRecord {
15
- toolCall?: DispatchRecordToolCall;
16
- promptSize?: number;
17
- originalPromptSize?: number;
18
- batchId?: string;
19
- rid?: string;
20
- role?: string;
21
- sessionId?: string;
22
- }
23
- export declare function registerSubAgentExecCommand(parent: Command, io: ProgramIO): void;
24
- export declare function readDispatchRecordForExec(dispatchRecordPath: string): DispatchRecord;
25
- export {};