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
|
@@ -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.
|
|
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-mut": "0.1.
|
|
102
|
-
"peaks-loop-shared-channel": "0.0.
|
|
103
|
-
"peaks-loop-shared": "0.0.
|
|
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"
|
|
@@ -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
|
|
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 —
|
|
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
|
|