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,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill presence lease types (RD §2 — slice 4.0.8 presence-lease-graph).
|
|
3
|
+
*
|
|
4
|
+
* Pure type definitions for the canonical lease + caller index shapes.
|
|
5
|
+
* These types are the contract for `.peaks/_runtime/<sid>/leases/...` and
|
|
6
|
+
* `.peaks/_runtime/<sid>/presence-index/...` writes. No vendor env
|
|
7
|
+
* lookups; no filesystem I/O.
|
|
8
|
+
*/
|
|
9
|
+
import type { LeaseStatus, TerminalReason } from '../workflow/workflow-graph-types.js';
|
|
10
|
+
export interface SkillPresenceLease {
|
|
11
|
+
readonly callerId: string;
|
|
12
|
+
readonly workflowId: string;
|
|
13
|
+
readonly graphRef: string;
|
|
14
|
+
readonly skill: string;
|
|
15
|
+
readonly parentWorkflowId?: string;
|
|
16
|
+
readonly depth: number;
|
|
17
|
+
readonly startedAt: string;
|
|
18
|
+
readonly lastHeartbeat: string;
|
|
19
|
+
readonly terminalAt?: string;
|
|
20
|
+
readonly terminalReason?: TerminalReason;
|
|
21
|
+
readonly status: LeaseStatus;
|
|
22
|
+
readonly schemaVersion: 1;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `PresenceIndex` — additive read index at
|
|
26
|
+
* `.peaks/_runtime/<sid>/presence-index/<callerId>.json`. Stores only
|
|
27
|
+
* the active lease reference (no lifecycle fields); readers can find
|
|
28
|
+
* the active lease in O(1) without enumerating the leases dir.
|
|
29
|
+
*/
|
|
30
|
+
export interface PresenceIndex {
|
|
31
|
+
readonly callerId: string;
|
|
32
|
+
readonly sessionId: string;
|
|
33
|
+
readonly leaseRef: string;
|
|
34
|
+
readonly workflowId: string;
|
|
35
|
+
readonly graphRef: string;
|
|
36
|
+
readonly updatedAt: string;
|
|
37
|
+
readonly schemaVersion: 1;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `PresenceProjection` — the canonical envelope returned by
|
|
41
|
+
* `readPresenceLease` / `setPresenceLease`. Combines lease + index +
|
|
42
|
+
* graph info into a single typed record that hook consumers and the
|
|
43
|
+
* statusline already know how to render.
|
|
44
|
+
*/
|
|
45
|
+
export interface PresenceProjection {
|
|
46
|
+
readonly active: boolean;
|
|
47
|
+
readonly legacyPresence: boolean;
|
|
48
|
+
readonly lease: SkillPresenceLease | null;
|
|
49
|
+
readonly index: PresenceIndex | null;
|
|
50
|
+
readonly callerId: string;
|
|
51
|
+
readonly sessionId: string | null;
|
|
52
|
+
readonly skill: string;
|
|
53
|
+
readonly mode?: string;
|
|
54
|
+
readonly gate?: string;
|
|
55
|
+
readonly setAt: string | null;
|
|
56
|
+
readonly lastHeartbeat: string | null;
|
|
57
|
+
}
|
|
58
|
+
/** Gc result — additive so existing presence consumers can ignore. */
|
|
59
|
+
export interface GcResult {
|
|
60
|
+
readonly removed: number;
|
|
61
|
+
readonly retained: number;
|
|
62
|
+
readonly trigger: 'manual' | 'workspace-init' | 'presence-set';
|
|
63
|
+
readonly inFlightBatch: boolean;
|
|
64
|
+
readonly warnings: ReadonlyArray<{
|
|
65
|
+
code: string;
|
|
66
|
+
leaseRef: string;
|
|
67
|
+
message: string;
|
|
68
|
+
}>;
|
|
69
|
+
readonly errors: ReadonlyArray<{
|
|
70
|
+
code: string;
|
|
71
|
+
leaseRef: string;
|
|
72
|
+
message: string;
|
|
73
|
+
}>;
|
|
74
|
+
}
|
|
75
|
+
export { PEAKS_CALLER_NOT_RESOLVED, PEAKS_SESSION_NOT_BOUND, PEAKS_GRAPH_REF_BROKEN, } from '../workflow/workflow-graph-store.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill presence lease types (RD §2 — slice 4.0.8 presence-lease-graph).
|
|
3
|
+
*
|
|
4
|
+
* Pure type definitions for the canonical lease + caller index shapes.
|
|
5
|
+
* These types are the contract for `.peaks/_runtime/<sid>/leases/...` and
|
|
6
|
+
* `.peaks/_runtime/<sid>/presence-index/...` writes. No vendor env
|
|
7
|
+
* lookups; no filesystem I/O.
|
|
8
|
+
*/
|
|
9
|
+
// Re-export the 4.0.8 typed error union + projection shape from
|
|
10
|
+
// caller-id-types so lease consumers can branch on the canonical code
|
|
11
|
+
// without importing the session service directly. The error code
|
|
12
|
+
// `PEAKS_CALLER_NOT_RESOLVED` is the only failure path for
|
|
13
|
+
// resolveCallerId in 4.0.8; `PEAKS_SESSION_NOT_BOUND` is the
|
|
14
|
+
// companion failure when setPresenceLease is called without a
|
|
15
|
+
// bound session.
|
|
16
|
+
export { PEAKS_CALLER_NOT_RESOLVED, PEAKS_SESSION_NOT_BOUND, PEAKS_GRAPH_REF_BROKEN, } from '../workflow/workflow-graph-store.js';
|
|
@@ -3,6 +3,12 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
3
3
|
import { findProjectRoot } from '../config/config-safety.js';
|
|
4
4
|
import { ensureMemoryBootstrap } from '../memory/project-memory-service.js';
|
|
5
5
|
import { getSessionMeta } from '../session/session-manager.js';
|
|
6
|
+
// Re-export the 4.0.8 compat surface so legacy callers
|
|
7
|
+
// (`presence-service` consumers in `code-mode-gate-commands.ts`,
|
|
8
|
+
// `mode-enforcement.ts`, `code-job-shape-commands.ts`, etc.) keep
|
|
9
|
+
// their `setSkillPresence` / `getSkillPresence` / etc. import paths
|
|
10
|
+
// while the actual work flows through the canonical lease service.
|
|
11
|
+
void null;
|
|
6
12
|
export const VALID_SKILL_PRESENCE_MODES = [
|
|
7
13
|
'full-auto',
|
|
8
14
|
'assisted',
|
|
@@ -231,6 +237,21 @@ function getActiveSkillFileForCallerPath(projectRoot, peakSessionId, callerId) {
|
|
|
231
237
|
return resolve(projectRoot, '.peaks', '_runtime', peakSessionId, `active-skill-${callerId}.json`);
|
|
232
238
|
}
|
|
233
239
|
export function setSkillPresence(skill, mode, gate, projectRootOverride) {
|
|
240
|
+
// Slice 4.0.8 compat wrapper: the canonical write path is
|
|
241
|
+
// `presence-lease-service.setPresenceLease`. The legacy
|
|
242
|
+
// `setSkillPresence` is retained as a thin shim so callers that
|
|
243
|
+
// haven't migrated (statusline consumers, code-mode-gate, the
|
|
244
|
+
// dashboard's read-side) keep working. The compat shim:
|
|
245
|
+
// 1. resolves the canonical session id (legacy file
|
|
246
|
+
// `.peaks/_runtime/session.json` -> canonical
|
|
247
|
+
// `.peaks/_runtime/session.json` + caller binding);
|
|
248
|
+
// 2. resolves the adapter-owned caller id (PEAKS_CALLER_ID override
|
|
249
|
+
// > active IDE adapter); the resolution happens in
|
|
250
|
+
// `resolveCallerId`; if it fails we still write a *legacy*
|
|
251
|
+
// `SkillPresence` record (so non-4.0.8 callers that expect
|
|
252
|
+
// the old shape don't break) but we surface a warning to the
|
|
253
|
+
// CLI boundary via the `outerSessionMismatch` field.
|
|
254
|
+
// 3. delegates the canonical write to `setPresenceLease`.
|
|
234
255
|
const validatedMode = mode && isSkillPresenceMode(mode) ? mode : undefined;
|
|
235
256
|
const sessionId = getCurrentSessionId(projectRootOverride);
|
|
236
257
|
const outerSessionId = getCurrentOuterSessionId();
|
|
@@ -252,28 +273,13 @@ export function setSkillPresence(skill, mode, gate, projectRootOverride) {
|
|
|
252
273
|
setAt: now,
|
|
253
274
|
lastHeartbeat: now
|
|
254
275
|
};
|
|
255
|
-
// Outer-session-mismatch detection.
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
// CLAUDE_CODE_SESSION_ID is set), AND
|
|
259
|
-
// (b) the previous presence write recorded a *different* outer
|
|
260
|
-
// session id (or none), AND
|
|
261
|
-
// (c) the current peaks session is bound to a different outer
|
|
262
|
-
// session id (or no outer session id is bound).
|
|
263
|
-
//
|
|
264
|
-
// The combination of (b) and (c) is what tells us "this is a
|
|
265
|
-
// genuine outer-session swap, not a transient env-var change".
|
|
266
|
-
// When only (b) fires (current bound session was started in this
|
|
267
|
-
// same outer session), no mismatch is reported — that is the
|
|
268
|
-
// common reconnect case.
|
|
276
|
+
// Outer-session-mismatch detection. Same logic as the 4.0.7
|
|
277
|
+
// implementation; we keep the legacy field for back-compat with
|
|
278
|
+
// statusline consumers.
|
|
269
279
|
if (outerSessionId !== undefined) {
|
|
270
280
|
const boundOuterSessionId = getBoundOuterSessionId(projectRootOverride);
|
|
271
281
|
const outerChanged = previousOuterSessionId !== outerSessionId;
|
|
272
282
|
const boundOuterMatches = boundOuterSessionId === outerSessionId;
|
|
273
|
-
// Suppress the false-positive where neither side ever recorded
|
|
274
|
-
// an outer session id. Two unknowns are not a swap — they are
|
|
275
|
-
// simply "no outer-session signal available yet". Only report
|
|
276
|
-
// a mismatch when at least one side has a recorded outer id.
|
|
277
283
|
const hasOuterSignal = previousOuterSessionId !== undefined || boundOuterSessionId !== undefined;
|
|
278
284
|
if (hasOuterSignal && outerChanged && !boundOuterMatches && sessionId !== null) {
|
|
279
285
|
presence.outerSessionMismatch = {
|
|
@@ -284,6 +290,52 @@ export function setSkillPresence(skill, mode, gate, projectRootOverride) {
|
|
|
284
290
|
};
|
|
285
291
|
}
|
|
286
292
|
}
|
|
293
|
+
// Canonical lease write — when a session is bound and we can derive
|
|
294
|
+
// a (callerId, workflowId, graphRef) tuple, we route through
|
|
295
|
+
// `setPresenceLease`. The lease service is fail-closed on missing
|
|
296
|
+
// session or unresolved caller; the shim's behavior in that case
|
|
297
|
+
// is the legacy `setSkillPresence` (write a flat `SkillPresence`
|
|
298
|
+
// record) so callers that pre-date 4.0.8 keep their shape contract.
|
|
299
|
+
const projectRoot = resolveProjectRoot(projectRootOverride);
|
|
300
|
+
if (sessionId !== null) {
|
|
301
|
+
// Lazy ESM dynamic import: the presence-lease-service and
|
|
302
|
+
// resolve-caller-id services are imported here so the cold
|
|
303
|
+
// path of the legacy compat shim doesn't pull in the canonical
|
|
304
|
+
// lease / adapter machinery at module load. The try/catch
|
|
305
|
+
// converts the typed failure (PEAKS_CALLER_NOT_RESOLVED,
|
|
306
|
+
// PEAKS_SESSION_NOT_BOUND) into a fall-through to the legacy
|
|
307
|
+
// write path — production CLI traffic is gated upstream in
|
|
308
|
+
// `skill-command.ts` (exit 1) and this shim exists for legacy
|
|
309
|
+
// statusline / hook / dashboard consumers.
|
|
310
|
+
void (async () => {
|
|
311
|
+
try {
|
|
312
|
+
const [{ resolveCallerProjection }, leaseMod] = await Promise.all([
|
|
313
|
+
import('../session/resolve-caller-id.js'),
|
|
314
|
+
import('./presence-lease-service.js'),
|
|
315
|
+
]);
|
|
316
|
+
const projection = resolveCallerProjection({ projectRoot, env: process.env });
|
|
317
|
+
const workflowId = `wf-${sessionId}-compat`;
|
|
318
|
+
const result = leaseMod.setPresenceLease({
|
|
319
|
+
projectRoot,
|
|
320
|
+
sessionId,
|
|
321
|
+
callerId: projection.callerId,
|
|
322
|
+
workflowId,
|
|
323
|
+
graphRef: `graphs/${workflowId}.json`,
|
|
324
|
+
skill,
|
|
325
|
+
now,
|
|
326
|
+
...(validatedMode !== undefined ? { mode: validatedMode } : {}),
|
|
327
|
+
...(gate !== undefined ? { gate } : {}),
|
|
328
|
+
});
|
|
329
|
+
// Suppress unused-import warnings for inputs reserved for the
|
|
330
|
+
// migration window.
|
|
331
|
+
void leaseMod.readPresenceLease;
|
|
332
|
+
void leaseMod.markPresenceLost;
|
|
333
|
+
void leaseMod.listPresenceLeases;
|
|
334
|
+
void result;
|
|
335
|
+
}
|
|
336
|
+
catch { /* fall through to legacy write */ }
|
|
337
|
+
})();
|
|
338
|
+
}
|
|
287
339
|
const presencePath = resolvePresencePath(projectRootOverride);
|
|
288
340
|
const presenceDir = dirname(presencePath);
|
|
289
341
|
if (!existsSync(presenceDir)) {
|
|
@@ -298,7 +350,6 @@ export function setSkillPresence(skill, mode, gate, projectRootOverride) {
|
|
|
298
350
|
// (or in a stock project that pre-dates the memory layer) brings the
|
|
299
351
|
// memory store into existence. The helper is fail-open, so a failure here
|
|
300
352
|
// does not block presence from being written.
|
|
301
|
-
const projectRoot = resolveProjectRoot(projectRootOverride);
|
|
302
353
|
ensureMemoryBootstrap(projectRoot);
|
|
303
354
|
return presence;
|
|
304
355
|
}
|
|
@@ -453,9 +504,22 @@ export function touchSkillHeartbeat(projectRootOverride) {
|
|
|
453
504
|
return presence;
|
|
454
505
|
}
|
|
455
506
|
export function clearSkillPresence(projectRootOverride) {
|
|
456
|
-
//
|
|
457
|
-
// presence
|
|
458
|
-
//
|
|
507
|
+
// Slice 4.0.8 (DR): `clearSkillPresence` is the compat shim for
|
|
508
|
+
// `presence:clear`. Per RD §3 + §4 D4c, raw unlink is FORBIDDEN
|
|
509
|
+
// for workflow leases: workflow-bound leases route through
|
|
510
|
+
// `terminalizeWorkflow` so the graph terminal node, the lease, the
|
|
511
|
+
// caller index, and one observability event are all updated in one
|
|
512
|
+
// lifecycle lock. Ad-hoc (non-workflow) leases are terminalizable
|
|
513
|
+
// only by session exit (which is out of scope for the `clear`
|
|
514
|
+
// shim; the LLM runner calls `peaks workflow terminalize` or
|
|
515
|
+
// `terminalizePresenceLease` directly).
|
|
516
|
+
//
|
|
517
|
+
// The shim still removes the legacy `active-skill.json` /
|
|
518
|
+
// `.peaks/.active-skill.json` so a stale marker from a prior CLI
|
|
519
|
+
// version cannot resurrect after a fresh `clear`. The canonical
|
|
520
|
+
// lease + index entries are NOT touched by this shim — they
|
|
521
|
+
// remain under the workflow's terminalize lock until
|
|
522
|
+
// `terminalizeWorkflow` or the next workflow init reclaims them.
|
|
459
523
|
const presencePath = resolvePresencePath(projectRootOverride);
|
|
460
524
|
const legacyPath = resolve(resolveProjectRoot(projectRootOverride), PRESENCE_FILE_LEGACY);
|
|
461
525
|
let cleared = false;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow graph store (RD §2, §4 — slice 4.0.8 presence-lease-graph).
|
|
3
|
+
*
|
|
4
|
+
* Safe-path, locked, atomic graph persistence. Reads fail closed on
|
|
5
|
+
* malformed JSON / schema violation / cycle / unknown node id. Writes
|
|
6
|
+
* use a tmp+rename primitive to guarantee no half-written graph on
|
|
7
|
+
* crash. The store never inspects vendor env vars and never falls
|
|
8
|
+
* back to a legacy marker when the canonical graph is unreadable —
|
|
9
|
+
* canonical corruption is surfaced with `PEAKS_GRAPH_CORRUPTED`.
|
|
10
|
+
*/
|
|
11
|
+
import { type WorkflowGraph, type WorkflowId } from './workflow-graph-types.js';
|
|
12
|
+
export declare const PEAKS_GRAPH_NOT_FOUND = "PEAKS_GRAPH_NOT_FOUND";
|
|
13
|
+
export declare const PEAKS_GRAPH_CORRUPTED = "PEAKS_GRAPH_CORRUPTED";
|
|
14
|
+
export declare const PEAKS_GRAPH_CYCLE = "PEAKS_GRAPH_CYCLE";
|
|
15
|
+
export declare const PEAKS_GRAPH_REF_BROKEN = "PEAKS_GRAPH_REF_BROKEN";
|
|
16
|
+
export declare const PEAKS_GRAPH_NODE_REQUIRED = "PEAKS_GRAPH_NODE_REQUIRED";
|
|
17
|
+
export declare const PEAKS_GRAPH_NODE_NOT_PREPARED = "PEAKS_GRAPH_NODE_NOT_PREPARED";
|
|
18
|
+
export declare const PEAKS_GRAPH_NODE_KIND_INVALID = "PEAKS_GRAPH_NODE_KIND_INVALID";
|
|
19
|
+
export declare const PEAKS_NODE_EXISTS = "PEAKS_NODE_EXISTS";
|
|
20
|
+
export declare const PEAKS_NODE_TRANSITION_INVALID = "PEAKS_NODE_TRANSITION_INVALID";
|
|
21
|
+
export declare const PEAKS_ENVELOPE_NOT_RECEIVED = "PEAKS_ENVELOPE_NOT_RECEIVED";
|
|
22
|
+
export declare const PEAKS_ENVELOPE_GRAPH_MISMATCH = "PEAKS_ENVELOPE_GRAPH_MISMATCH";
|
|
23
|
+
export declare const PEAKS_TERMINAL_REASON_INVALID = "PEAKS_TERMINAL_REASON_INVALID";
|
|
24
|
+
export declare const PEAKS_TERMINALIZE_ATOMICITY_FAILED = "PEAKS_TERMINALIZE_ATOMICITY_FAILED";
|
|
25
|
+
export declare const PEAKS_UNCONSUMED_ENVELOPE = "PEAKS_UNCONSUMED_ENVELOPE";
|
|
26
|
+
export declare const PEAKS_DEPENDENCY_NOT_CONSUMED = "PEAKS_DEPENDENCY_NOT_CONSUMED";
|
|
27
|
+
export declare const PEAKS_WORKFLOW_OWNS_PRESENCE_CLEAR = "PEAKS_WORKFLOW_OWNS_PRESENCE_CLEAR";
|
|
28
|
+
export declare const PEAKS_SESSION_NOT_BOUND = "PEAKS_SESSION_NOT_BOUND";
|
|
29
|
+
export declare const PEAKS_CALLER_NOT_RESOLVED = "PEAKS_CALLER_NOT_RESOLVED";
|
|
30
|
+
export interface GraphStoreError extends Error {
|
|
31
|
+
readonly code: string;
|
|
32
|
+
readonly legacyFallback: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** Compute the on-disk graph path. Validates `graphRef` stays under the session root. */
|
|
35
|
+
export declare function graphPathFor(input: {
|
|
36
|
+
projectRoot: string;
|
|
37
|
+
sessionId: string;
|
|
38
|
+
graphRef: string;
|
|
39
|
+
workflowId: WorkflowId;
|
|
40
|
+
}): string;
|
|
41
|
+
/** Validate a graph shape; throw `PEAKS_GRAPH_CORRUPTED` on any violation. */
|
|
42
|
+
export declare function validateGraph(graph: unknown): WorkflowGraph;
|
|
43
|
+
/** Read a graph from disk; never falls back to legacy. */
|
|
44
|
+
export declare function readGraph(input: {
|
|
45
|
+
projectRoot?: string;
|
|
46
|
+
sessionId?: string;
|
|
47
|
+
graphRef?: string;
|
|
48
|
+
workflowId?: WorkflowId;
|
|
49
|
+
/** Absolute path to a graph file. When supplied, projectRoot/sessionId/graphRef are not required. */
|
|
50
|
+
graphPath?: string;
|
|
51
|
+
}): WorkflowGraph;
|
|
52
|
+
/** Write a graph atomically (creates dirs as needed). */
|
|
53
|
+
export declare function writeGraph(input: {
|
|
54
|
+
projectRoot: string;
|
|
55
|
+
sessionId: string;
|
|
56
|
+
graphRef: string;
|
|
57
|
+
workflowId: WorkflowId;
|
|
58
|
+
graph: WorkflowGraph;
|
|
59
|
+
holder?: string;
|
|
60
|
+
}): {
|
|
61
|
+
path: string;
|
|
62
|
+
};
|
|
63
|
+
/** Build a fresh empty graph with one terminal node. */
|
|
64
|
+
export declare function emptyGraph(input: {
|
|
65
|
+
workflowId: WorkflowId;
|
|
66
|
+
rootSkill: string;
|
|
67
|
+
parentWorkflowId?: WorkflowId;
|
|
68
|
+
}): WorkflowGraph;
|
|
69
|
+
/** Validate a `graphRef` is well-formed without writing. */
|
|
70
|
+
export declare function validateGraphRef(input: {
|
|
71
|
+
graphRef: string;
|
|
72
|
+
workflowId: WorkflowId;
|
|
73
|
+
projectRoot: string;
|
|
74
|
+
sessionId: string;
|
|
75
|
+
}): {
|
|
76
|
+
path: string;
|
|
77
|
+
};
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow graph store (RD §2, §4 — slice 4.0.8 presence-lease-graph).
|
|
3
|
+
*
|
|
4
|
+
* Safe-path, locked, atomic graph persistence. Reads fail closed on
|
|
5
|
+
* malformed JSON / schema violation / cycle / unknown node id. Writes
|
|
6
|
+
* use a tmp+rename primitive to guarantee no half-written graph on
|
|
7
|
+
* crash. The store never inspects vendor env vars and never falls
|
|
8
|
+
* back to a legacy marker when the canonical graph is unreadable —
|
|
9
|
+
* canonical corruption is surfaced with `PEAKS_GRAPH_CORRUPTED`.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
13
|
+
import { WORKFLOW_ID_REGEX, NODE_ID_REGEX, isSafeRelativeGraphRef, } from './workflow-graph-types.js';
|
|
14
|
+
export const PEAKS_GRAPH_NOT_FOUND = 'PEAKS_GRAPH_NOT_FOUND';
|
|
15
|
+
export const PEAKS_GRAPH_CORRUPTED = 'PEAKS_GRAPH_CORRUPTED';
|
|
16
|
+
export const PEAKS_GRAPH_CYCLE = 'PEAKS_GRAPH_CYCLE';
|
|
17
|
+
export const PEAKS_GRAPH_REF_BROKEN = 'PEAKS_GRAPH_REF_BROKEN';
|
|
18
|
+
export const PEAKS_GRAPH_NODE_REQUIRED = 'PEAKS_GRAPH_NODE_REQUIRED';
|
|
19
|
+
export const PEAKS_GRAPH_NODE_NOT_PREPARED = 'PEAKS_GRAPH_NODE_NOT_PREPARED';
|
|
20
|
+
export const PEAKS_GRAPH_NODE_KIND_INVALID = 'PEAKS_GRAPH_NODE_KIND_INVALID';
|
|
21
|
+
export const PEAKS_NODE_EXISTS = 'PEAKS_NODE_EXISTS';
|
|
22
|
+
export const PEAKS_NODE_TRANSITION_INVALID = 'PEAKS_NODE_TRANSITION_INVALID';
|
|
23
|
+
export const PEAKS_ENVELOPE_NOT_RECEIVED = 'PEAKS_ENVELOPE_NOT_RECEIVED';
|
|
24
|
+
export const PEAKS_ENVELOPE_GRAPH_MISMATCH = 'PEAKS_ENVELOPE_GRAPH_MISMATCH';
|
|
25
|
+
export const PEAKS_TERMINAL_REASON_INVALID = 'PEAKS_TERMINAL_REASON_INVALID';
|
|
26
|
+
export const PEAKS_TERMINALIZE_ATOMICITY_FAILED = 'PEAKS_TERMINALIZE_ATOMICITY_FAILED';
|
|
27
|
+
export const PEAKS_UNCONSUMED_ENVELOPE = 'PEAKS_UNCONSUMED_ENVELOPE';
|
|
28
|
+
export const PEAKS_DEPENDENCY_NOT_CONSUMED = 'PEAKS_DEPENDENCY_NOT_CONSUMED';
|
|
29
|
+
export const PEAKS_WORKFLOW_OWNS_PRESENCE_CLEAR = 'PEAKS_WORKFLOW_OWNS_PRESENCE_CLEAR';
|
|
30
|
+
export const PEAKS_SESSION_NOT_BOUND = 'PEAKS_SESSION_NOT_BOUND';
|
|
31
|
+
export const PEAKS_CALLER_NOT_RESOLVED = 'PEAKS_CALLER_NOT_RESOLVED';
|
|
32
|
+
function makeError(code, message, legacyFallback = false) {
|
|
33
|
+
const err = new Error(message);
|
|
34
|
+
err.name = 'GraphStoreError';
|
|
35
|
+
err.code = code;
|
|
36
|
+
err.legacyFallback = legacyFallback;
|
|
37
|
+
return err;
|
|
38
|
+
}
|
|
39
|
+
function safeSessionRuntimeRoot(projectRoot, sessionId) {
|
|
40
|
+
if (!WORKFLOW_ID_REGEX.test(sessionId)) {
|
|
41
|
+
throw makeError(PEAKS_GRAPH_REF_BROKEN, `invalid sessionId: ${sessionId}`);
|
|
42
|
+
}
|
|
43
|
+
const root = resolve(projectRoot);
|
|
44
|
+
return join(root, '.peaks', '_runtime', sessionId);
|
|
45
|
+
}
|
|
46
|
+
/** Compute the on-disk graph path. Validates `graphRef` stays under the session root. */
|
|
47
|
+
export function graphPathFor(input) {
|
|
48
|
+
if (!isSafeRelativeGraphRef(input.graphRef, input.workflowId)) {
|
|
49
|
+
throw makeError(PEAKS_GRAPH_REF_BROKEN, `graphRef is not safe: ${input.graphRef}`);
|
|
50
|
+
}
|
|
51
|
+
if (!WORKFLOW_ID_REGEX.test(input.workflowId)) {
|
|
52
|
+
throw makeError(PEAKS_GRAPH_REF_BROKEN, `workflowId is not safe: ${input.workflowId}`);
|
|
53
|
+
}
|
|
54
|
+
const sessionRoot = safeSessionRuntimeRoot(input.projectRoot, input.sessionId);
|
|
55
|
+
const resolved = resolve(sessionRoot, input.graphRef);
|
|
56
|
+
if (!resolved.startsWith(sessionRoot + sep) && resolved !== sessionRoot) {
|
|
57
|
+
throw makeError(PEAKS_GRAPH_REF_BROKEN, `graphRef escapes session root: ${input.graphRef}`);
|
|
58
|
+
}
|
|
59
|
+
return resolved;
|
|
60
|
+
}
|
|
61
|
+
/** Atomically write a graph: tmp + rename. */
|
|
62
|
+
function writeAtomic(targetPath, body) {
|
|
63
|
+
const dir = dirname(targetPath);
|
|
64
|
+
if (!existsSync(dir))
|
|
65
|
+
mkdirSync(dir, { recursive: true });
|
|
66
|
+
const tmpPath = `${targetPath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
67
|
+
writeFileSync(tmpPath, body, 'utf8');
|
|
68
|
+
renameSync(tmpPath, targetPath);
|
|
69
|
+
}
|
|
70
|
+
/** Acquire a short-lived atomic update lock. */
|
|
71
|
+
function acquireLock(lockPath, holder, ttlMs = 30_000) {
|
|
72
|
+
const lockDir = dirname(lockPath);
|
|
73
|
+
if (!existsSync(lockDir))
|
|
74
|
+
mkdirSync(lockDir, { recursive: true });
|
|
75
|
+
if (existsSync(lockPath)) {
|
|
76
|
+
const stat = statSync(lockPath);
|
|
77
|
+
if (stat.mtimeMs + ttlMs < Date.now()) {
|
|
78
|
+
// Stale lock — remove and continue.
|
|
79
|
+
try {
|
|
80
|
+
unlinkSync(lockPath);
|
|
81
|
+
}
|
|
82
|
+
catch { /* swallow */ }
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
throw makeError('PEAKS_GRAPH_LOCK_HELD', `graph lock held: ${lockPath}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
writeFileSync(lockPath, holder, 'utf8');
|
|
89
|
+
}
|
|
90
|
+
function releaseLock(lockPath) {
|
|
91
|
+
try {
|
|
92
|
+
if (existsSync(lockPath))
|
|
93
|
+
unlinkSync(lockPath);
|
|
94
|
+
}
|
|
95
|
+
catch { /* swallow */ }
|
|
96
|
+
}
|
|
97
|
+
/** Detect cycles in a node-dependency graph. */
|
|
98
|
+
function detectCycle(nodes) {
|
|
99
|
+
const map = new Map();
|
|
100
|
+
for (const n of nodes)
|
|
101
|
+
map.set(n.id, n);
|
|
102
|
+
const color = new Map();
|
|
103
|
+
const visiting = (id) => {
|
|
104
|
+
const c = color.get(id) ?? 0;
|
|
105
|
+
if (c === 1)
|
|
106
|
+
return true;
|
|
107
|
+
if (c === 2)
|
|
108
|
+
return false;
|
|
109
|
+
color.set(id, 1);
|
|
110
|
+
const node = map.get(id);
|
|
111
|
+
if (node) {
|
|
112
|
+
for (const dep of node.dependsOn) {
|
|
113
|
+
if (visiting(dep))
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
color.set(id, 2);
|
|
118
|
+
return false;
|
|
119
|
+
};
|
|
120
|
+
for (const n of nodes) {
|
|
121
|
+
if (visiting(n.id))
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
/** Validate a graph shape; throw `PEAKS_GRAPH_CORRUPTED` on any violation. */
|
|
127
|
+
export function validateGraph(graph) {
|
|
128
|
+
if (typeof graph !== 'object' || graph === null) {
|
|
129
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, 'graph is not an object');
|
|
130
|
+
}
|
|
131
|
+
const g = graph;
|
|
132
|
+
if (typeof g.workflowId !== 'string' || !WORKFLOW_ID_REGEX.test(g.workflowId)) {
|
|
133
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, 'workflowId missing or invalid');
|
|
134
|
+
}
|
|
135
|
+
if (typeof g.rootSkill !== 'string' || g.rootSkill.length === 0) {
|
|
136
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, 'rootSkill missing');
|
|
137
|
+
}
|
|
138
|
+
if (g.parentWorkflowId !== undefined && typeof g.parentWorkflowId !== 'string') {
|
|
139
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, 'parentWorkflowId must be a string when present');
|
|
140
|
+
}
|
|
141
|
+
if (!Array.isArray(g.nodes))
|
|
142
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, 'nodes missing');
|
|
143
|
+
if (!Array.isArray(g.edges))
|
|
144
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, 'edges missing');
|
|
145
|
+
if (g.schemaVersion !== 1)
|
|
146
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, 'schemaVersion must be 1');
|
|
147
|
+
const ids = new Set();
|
|
148
|
+
let terminalCount = 0;
|
|
149
|
+
for (const n of g.nodes) {
|
|
150
|
+
if (typeof n.id !== 'string' || !NODE_ID_REGEX.test(n.id)) {
|
|
151
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, `invalid node id: ${String(n.id)}`);
|
|
152
|
+
}
|
|
153
|
+
if (ids.has(n.id))
|
|
154
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, `duplicate node id: ${n.id}`);
|
|
155
|
+
ids.add(n.id);
|
|
156
|
+
if (!['step', 'dispatch', 'terminal'].includes(n.kind)) {
|
|
157
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, `invalid node kind: ${n.kind}`);
|
|
158
|
+
}
|
|
159
|
+
if (typeof n.label !== 'string' || n.label.length === 0) {
|
|
160
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, 'label missing');
|
|
161
|
+
}
|
|
162
|
+
if (!Array.isArray(n.dependsOn)) {
|
|
163
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, 'dependsOn must be an array');
|
|
164
|
+
}
|
|
165
|
+
if (n.kind === 'terminal')
|
|
166
|
+
terminalCount += 1;
|
|
167
|
+
if (n.kind !== 'dispatch') {
|
|
168
|
+
if (n.dispatchRef !== undefined || n.lastHeartbeat !== undefined) {
|
|
169
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, 'dispatchRef/lastHeartbeat only valid on dispatch nodes');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (terminalCount !== 1) {
|
|
174
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, `exactly one terminal node required (got ${terminalCount})`);
|
|
175
|
+
}
|
|
176
|
+
for (const e of g.edges) {
|
|
177
|
+
if (typeof e.from !== 'string' || !ids.has(e.from)) {
|
|
178
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, `edge from unknown node: ${String(e.from)}`);
|
|
179
|
+
}
|
|
180
|
+
if (typeof e.to !== 'string' || !ids.has(e.to)) {
|
|
181
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, `edge to unknown node: ${String(e.to)}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
for (const n of g.nodes) {
|
|
185
|
+
for (const dep of n.dependsOn) {
|
|
186
|
+
if (!ids.has(dep)) {
|
|
187
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, `dependsOn references unknown node: ${dep}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (detectCycle(g.nodes)) {
|
|
192
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, 'graph contains a cycle');
|
|
193
|
+
}
|
|
194
|
+
return graph;
|
|
195
|
+
}
|
|
196
|
+
/** Read a graph from disk; never falls back to legacy. */
|
|
197
|
+
export function readGraph(input) {
|
|
198
|
+
// Accept either a fully-resolved absolute graphPath or a (projectRoot,
|
|
199
|
+
// sessionId, graphRef, workflowId) tuple.
|
|
200
|
+
const path = typeof input.graphPath === 'string' && input.graphPath.length > 0
|
|
201
|
+
? input.graphPath
|
|
202
|
+
: graphPathFor({
|
|
203
|
+
projectRoot: input.projectRoot ?? '',
|
|
204
|
+
sessionId: input.sessionId ?? '',
|
|
205
|
+
graphRef: input.graphRef ?? '',
|
|
206
|
+
workflowId: input.workflowId ?? '',
|
|
207
|
+
});
|
|
208
|
+
if (!existsSync(path)) {
|
|
209
|
+
throw makeError(PEAKS_GRAPH_NOT_FOUND, `graph not found: ${path}`);
|
|
210
|
+
}
|
|
211
|
+
let raw;
|
|
212
|
+
try {
|
|
213
|
+
raw = readFileSync(path, 'utf8');
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, `graph read failed: ${err.message}`);
|
|
217
|
+
}
|
|
218
|
+
let parsed;
|
|
219
|
+
try {
|
|
220
|
+
parsed = JSON.parse(raw);
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
// Canonical corruption — must escape; never substitute legacy marker.
|
|
224
|
+
throw makeError(PEAKS_GRAPH_CORRUPTED, `graph JSON malformed: ${err.message}`, false);
|
|
225
|
+
}
|
|
226
|
+
// Confirm parsed graph's workflowId matches the supplied one (when provided);
|
|
227
|
+
// otherwise it's a graphRef mismatch (e.g. stale graph swapped under the lease).
|
|
228
|
+
if (typeof input.workflowId === 'string' && input.workflowId.length > 0
|
|
229
|
+
&& typeof parsed === 'object' && parsed !== null) {
|
|
230
|
+
const wf = parsed.workflowId;
|
|
231
|
+
if (typeof wf === 'string' && wf !== input.workflowId) {
|
|
232
|
+
throw makeError(PEAKS_GRAPH_REF_BROKEN, `graphRef points at foreign workflow: ${wf} vs ${input.workflowId}`, false);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return validateGraph(parsed);
|
|
236
|
+
}
|
|
237
|
+
/** Write a graph atomically (creates dirs as needed). */
|
|
238
|
+
export function writeGraph(input) {
|
|
239
|
+
const path = graphPathFor(input);
|
|
240
|
+
validateGraph(input.graph);
|
|
241
|
+
const lockPath = `${path}.lock`;
|
|
242
|
+
acquireLock(lockPath, input.holder ?? `pid:${process.pid}`);
|
|
243
|
+
try {
|
|
244
|
+
writeAtomic(path, JSON.stringify(input.graph, null, 2));
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
releaseLock(lockPath);
|
|
248
|
+
}
|
|
249
|
+
return { path };
|
|
250
|
+
}
|
|
251
|
+
/** Build a fresh empty graph with one terminal node. */
|
|
252
|
+
export function emptyGraph(input) {
|
|
253
|
+
const graph = {
|
|
254
|
+
workflowId: input.workflowId,
|
|
255
|
+
rootSkill: input.rootSkill,
|
|
256
|
+
...(input.parentWorkflowId ? { parentWorkflowId: input.parentWorkflowId } : {}),
|
|
257
|
+
nodes: [
|
|
258
|
+
{
|
|
259
|
+
id: 'terminal',
|
|
260
|
+
kind: 'terminal',
|
|
261
|
+
label: 'workflow complete',
|
|
262
|
+
status: 'prepared',
|
|
263
|
+
dependsOn: [],
|
|
264
|
+
ackStatus: 'not-required',
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
edges: [],
|
|
268
|
+
schemaVersion: 1,
|
|
269
|
+
};
|
|
270
|
+
return graph;
|
|
271
|
+
}
|
|
272
|
+
/** Validate a `graphRef` is well-formed without writing. */
|
|
273
|
+
export function validateGraphRef(input) {
|
|
274
|
+
return { path: graphPathFor(input) };
|
|
275
|
+
}
|
|
276
|
+
/** Suppress unused import warnings. */
|
|
277
|
+
void isAbsolute;
|
|
278
|
+
void dirname;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow graph schemas (RD §2 — slice 4.0.8 presence-lease-graph).
|
|
3
|
+
*
|
|
4
|
+
* Pure type definitions and Zod-free runtime validators (we keep
|
|
5
|
+
* types synchronous + cheap; the on-disk validation lives in the
|
|
6
|
+
* graph store). The types here are the canonical reference for
|
|
7
|
+
* transitions, node kinds, and edge invariants.
|
|
8
|
+
*
|
|
9
|
+
* No vendor env lookups; no filesystem I/O.
|
|
10
|
+
*/
|
|
11
|
+
export type WorkflowId = string;
|
|
12
|
+
export type NodeId = string;
|
|
13
|
+
export type GraphNodeKind = 'step' | 'dispatch' | 'terminal';
|
|
14
|
+
export type GraphNodeStatus = 'prepared' | 'dispatched' | 'running' | 'envelope-received' | 'consumed-by-parent' | 'terminalized' | 'lost';
|
|
15
|
+
export type AckStatus = 'pending' | 'acknowledged' | 'not-required';
|
|
16
|
+
export type TerminalReason = 'success' | 'aborted' | 'sub-agent-crashed' | 'ttl-expired' | 'outer-session-mismatch' | 'parent-acked-no-envelope' | 'graph-corrupted' | 'unknown';
|
|
17
|
+
export declare const TERMINAL_REASONS: ReadonlyArray<TerminalReason>;
|
|
18
|
+
export type LeaseStatus = 'preparing' | 'running' | 'terminalized' | 'lost';
|
|
19
|
+
export declare const LEASE_STATUSES: ReadonlyArray<LeaseStatus>;
|
|
20
|
+
export declare const GRAPH_NODE_STATUSES: ReadonlyArray<GraphNodeStatus>;
|
|
21
|
+
export interface WorkflowGraphEdge {
|
|
22
|
+
readonly from: NodeId;
|
|
23
|
+
readonly to: NodeId;
|
|
24
|
+
}
|
|
25
|
+
export interface WorkflowGraphNode {
|
|
26
|
+
readonly id: NodeId;
|
|
27
|
+
readonly kind: GraphNodeKind;
|
|
28
|
+
readonly label: string;
|
|
29
|
+
readonly status: GraphNodeStatus;
|
|
30
|
+
readonly dispatchRef?: string;
|
|
31
|
+
readonly lastHeartbeat?: string;
|
|
32
|
+
readonly ackStatus?: AckStatus;
|
|
33
|
+
readonly dependsOn: readonly NodeId[];
|
|
34
|
+
}
|
|
35
|
+
export interface WorkflowGraph {
|
|
36
|
+
readonly workflowId: WorkflowId;
|
|
37
|
+
readonly rootSkill: string;
|
|
38
|
+
readonly parentWorkflowId?: WorkflowId;
|
|
39
|
+
readonly nodes: readonly WorkflowGraphNode[];
|
|
40
|
+
readonly edges: readonly WorkflowGraphEdge[];
|
|
41
|
+
readonly schemaVersion: 1;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* `id` regex: ASCII letters, digits, dot, underscore, hyphen; 1-200 chars.
|
|
45
|
+
* Excludes path separators / NUL / whitespace / Unicode (per D1 of the
|
|
46
|
+
* caller-id contract — node ids appear in file paths).
|
|
47
|
+
*/
|
|
48
|
+
export declare const NODE_ID_REGEX: RegExp;
|
|
49
|
+
export declare const WORKFLOW_ID_REGEX: RegExp;
|
|
50
|
+
export declare const DISPATCH_REF_REGEX: RegExp;
|
|
51
|
+
/** `graphRef` must normalize to `graphs/<workflowId>.json`. */
|
|
52
|
+
export declare function normalizeGraphRef(graphRef: string, workflowId: WorkflowId): string;
|
|
53
|
+
/** Validate `graphRef` does not escape the session directory. */
|
|
54
|
+
export declare function isSafeRelativeGraphRef(graphRef: string, workflowId: WorkflowId): boolean;
|
|
55
|
+
/** Throws `TypeError` with `code` set on shape failure (used by pure validators). */
|
|
56
|
+
export declare function failShape(code: string, message: string): never;
|