peaks-loop 4.0.7 → 4.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -288
- package/dist/cli/commands/_register.js +2 -0
- package/dist/cli/commands/baseline-commands.d.ts +3 -0
- package/dist/cli/commands/baseline-commands.js +146 -0
- package/dist/cli/commands/code-job-shape-commands.js +8 -11
- package/dist/cli/commands/code-mode-gate-commands.js +1 -43
- package/dist/cli/commands/code-runtime-commands.js +36 -14
- package/dist/cli/commands/core/skill-command.js +75 -13
- package/dist/cli/commands/core/standards-command.js +5 -50
- package/dist/cli/commands/dispatch-commands.d.ts +20 -0
- package/dist/cli/commands/dispatch-commands.js +39 -38
- package/dist/cli/commands/dispatch-from-dag.js +17 -0
- package/dist/cli/commands/heartbeat-commands.d.ts +21 -0
- package/dist/cli/commands/heartbeat-commands.js +40 -0
- package/dist/cli/commands/hook-handle.js +5 -2
- package/dist/cli/commands/hooks-commands.js +9 -26
- package/dist/cli/commands/loop-eval-commands.js +5 -2
- package/dist/cli/commands/statusline-commands.js +1 -1
- package/dist/cli/commands/sub-agent-commands.js +0 -2
- package/dist/cli/commands/sub-agent-shared.d.ts +7 -9
- package/dist/cli/commands/workflow-commands.js +8 -0
- package/dist/cli/commands/workflow-lifecycle-commands.d.ts +63 -0
- package/dist/cli/commands/workflow-lifecycle-commands.js +302 -0
- package/dist/cli/commands/workspace/init-command.js +19 -0
- package/dist/services/audit/backing-detector.d.ts +2 -19
- package/dist/services/audit/backing-detector.js +7 -66
- package/dist/services/audit/enforcers/active-skill-resolver.d.ts +25 -9
- package/dist/services/audit/enforcers/active-skill-resolver.js +83 -21
- package/dist/services/audit/enforcers/lint-audit-regression.js +1 -10
- package/dist/services/capability-audit-service/cross-check.d.ts +8 -0
- package/dist/services/capability-audit-service/cross-check.js +11 -0
- package/dist/services/capability-audit-service/index.d.ts +3 -0
- package/dist/services/capability-audit-service/index.js +2 -0
- package/dist/services/capability-audit-service/runner.d.ts +27 -0
- package/dist/services/capability-audit-service/runner.js +40 -0
- package/dist/services/capability-audit-service/staleness.d.ts +1 -0
- package/dist/services/capability-audit-service/staleness.js +7 -0
- package/dist/services/capability-audit-service/types.d.ts +24 -0
- package/dist/services/capability-audit-service/types.js +1 -0
- package/dist/services/capability-baseline/store.d.ts +33 -0
- package/dist/services/capability-baseline/store.js +80 -0
- package/dist/services/capability-baseline/types.d.ts +45 -0
- package/dist/services/capability-baseline/types.js +5 -0
- package/dist/services/capability-baseline/validator.d.ts +19 -0
- package/dist/services/capability-baseline/validator.js +38 -0
- package/dist/services/capability-guard-runner/contracts/J01.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J01.js +48 -0
- package/dist/services/capability-guard-runner/contracts/J02.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J02.js +34 -0
- package/dist/services/capability-guard-runner/contracts/J03.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J03.js +24 -0
- package/dist/services/capability-guard-runner/contracts/J04.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J04.js +40 -0
- package/dist/services/capability-guard-runner/contracts/J05.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J05.js +21 -0
- package/dist/services/capability-guard-runner/contracts/J06.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J06.js +43 -0
- package/dist/services/capability-guard-runner/contracts/J07.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J07.js +53 -0
- package/dist/services/capability-guard-runner/contracts/J08.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J08.js +43 -0
- package/dist/services/capability-guard-runner/contracts/J09.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J09.js +44 -0
- package/dist/services/capability-guard-runner/contracts/J10.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J10.js +40 -0
- package/dist/services/capability-guard-runner/contracts/J11.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J11.js +39 -0
- package/dist/services/capability-guard-runner/contracts/J12.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J12.js +34 -0
- package/dist/services/capability-guard-runner/contracts/J13.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J13.js +45 -0
- package/dist/services/capability-guard-runner/contracts/J14.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J14.js +36 -0
- package/dist/services/capability-guard-runner/contracts/J15.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J15.js +40 -0
- package/dist/services/capability-guard-runner/diff.d.ts +2 -0
- package/dist/services/capability-guard-runner/diff.js +7 -0
- package/dist/services/capability-guard-runner/runner.d.ts +9 -0
- package/dist/services/capability-guard-runner/runner.js +22 -0
- package/dist/services/capability-guard-runner/types.d.ts +39 -0
- package/dist/services/capability-guard-runner/types.js +1 -0
- package/dist/services/code/auto-compact-orchestrator.d.ts +35 -2
- package/dist/services/code/auto-compact-orchestrator.js +29 -9
- package/dist/services/context/build-dispatch-system-prompt.js +0 -18
- package/dist/services/dispatch/dispatch-record-writer.d.ts +25 -1
- package/dist/services/dispatch/dispatch-record-writer.js +85 -15
- package/dist/services/dispatch/test-tool-detection.d.ts +0 -14
- package/dist/services/dispatch/test-tool-detection.js +0 -22
- package/dist/services/doctor/doctor-service/checks/skill-presence.d.ts +8 -0
- package/dist/services/doctor/doctor-service/checks/skill-presence.js +9 -1
- package/dist/services/evolution/evolution-types.d.ts +16 -16
- package/dist/services/final-review/final-review-service.d.ts +8 -0
- package/dist/services/final-review/final-review-service.js +14 -0
- package/dist/services/final-review/final-review-types.d.ts +1 -1
- package/dist/services/hooks/presence-marker-detector.d.ts +6 -0
- package/dist/services/hooks/presence-marker-detector.js +48 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +20 -0
- package/dist/services/ide/adapters/codex-adapter.js +20 -1
- package/dist/services/ide/adapters/cursor-adapter.js +21 -1
- package/dist/services/ide/adapters/hermes-adapter.js +20 -1
- package/dist/services/ide/adapters/openclaw-adapter.js +20 -1
- package/dist/services/ide/adapters/qoder-adapter.js +20 -1
- package/dist/services/ide/adapters/tongyi-lingma-adapter.js +20 -1
- package/dist/services/ide/adapters/trae-adapter.js +21 -1
- package/dist/services/ide/adapters/zcode-adapter.js +19 -0
- package/dist/services/ide/ide-types.d.ts +15 -0
- package/dist/services/job/job-progress-store.d.ts +2 -2
- package/dist/services/job/job-types.d.ts +26 -26
- package/dist/services/observability/observability-service.d.ts +4 -4
- package/dist/services/scan/archetype-service.js +2 -52
- package/dist/services/sediment/json-schema.d.ts +4 -4
- package/dist/services/session/caller-binding-service.d.ts +21 -0
- package/dist/services/session/caller-binding-service.js +35 -0
- package/dist/services/session/caller-id-types.d.ts +62 -13
- package/dist/services/session/caller-id-types.js +25 -13
- package/dist/services/session/index.d.ts +4 -2
- package/dist/services/session/index.js +8 -2
- package/dist/services/session/platform-fallbacks.d.ts +25 -17
- package/dist/services/session/platform-fallbacks.js +26 -28
- package/dist/services/session/resolve-caller-id.d.ts +64 -29
- package/dist/services/session/resolve-caller-id.js +122 -48
- package/dist/services/session/session-manager.d.ts +0 -29
- package/dist/services/session/session-manager.js +0 -33
- package/dist/services/session/session-resume-service.d.ts +4 -0
- package/dist/services/session/session-resume-service.js +4 -0
- package/dist/services/skills/hooks-settings-service.d.ts +0 -41
- package/dist/services/skills/hooks-settings-service.js +5 -61
- package/dist/services/skills/presence-lease-service.d.ts +73 -0
- package/dist/services/skills/presence-lease-service.js +358 -0
- package/dist/services/skills/presence-lease-types.d.ts +75 -0
- package/dist/services/skills/presence-lease-types.js +16 -0
- package/dist/services/skills/skill-presence-service.js +86 -22
- package/dist/services/skills/skill-statusline-renderer.d.ts +46 -56
- package/dist/services/skills/skill-statusline-renderer.js +312 -100
- package/dist/services/slice/slice-check-service.js +0 -59
- package/dist/services/standards/project-standards-service.js +0 -44
- package/dist/services/workflow/workflow-graph-store.d.ts +77 -0
- package/dist/services/workflow/workflow-graph-store.js +278 -0
- package/dist/services/workflow/workflow-graph-types.d.ts +56 -0
- package/dist/services/workflow/workflow-graph-types.js +67 -0
- package/dist/services/workflow/workflow-inflight-probe.d.ts +52 -0
- package/dist/services/workflow/workflow-inflight-probe.js +86 -0
- package/dist/services/workflow/workflow-node-lifecycle.d.ts +67 -0
- package/dist/services/workflow/workflow-node-lifecycle.js +348 -0
- package/dist/services/workflow/workflow-presence-lifecycle.d.ts +58 -0
- package/dist/services/workflow/workflow-presence-lifecycle.js +196 -0
- package/dist/services/workspace/reconcile-service.d.ts +31 -0
- package/dist/services/workspace/reconcile-service.js +132 -1
- package/package.json +5 -4
- package/skills/peaks-code/SKILL.md +5 -43
- package/skills/peaks-code/references/completion-handoff.md +1 -1
- package/skills/peaks-code/references/skill-presence-and-title.md +1 -1
- package/dist/cli/commands/sub-agent-exec-command.d.ts +0 -25
- package/dist/cli/commands/sub-agent-exec-command.js +0 -132
- package/dist/services/standards/find-skills-integration.d.ts +0 -59
- package/dist/services/standards/find-skills-integration.js +0 -225
|
@@ -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;
|
|
@@ -0,0 +1,67 @@
|
|
|
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 const TERMINAL_REASONS = [
|
|
12
|
+
'success',
|
|
13
|
+
'aborted',
|
|
14
|
+
'sub-agent-crashed',
|
|
15
|
+
'ttl-expired',
|
|
16
|
+
'outer-session-mismatch',
|
|
17
|
+
'parent-acked-no-envelope',
|
|
18
|
+
'graph-corrupted',
|
|
19
|
+
'unknown',
|
|
20
|
+
];
|
|
21
|
+
export const LEASE_STATUSES = [
|
|
22
|
+
'preparing',
|
|
23
|
+
'running',
|
|
24
|
+
'terminalized',
|
|
25
|
+
'lost',
|
|
26
|
+
];
|
|
27
|
+
export const GRAPH_NODE_STATUSES = [
|
|
28
|
+
'prepared',
|
|
29
|
+
'dispatched',
|
|
30
|
+
'running',
|
|
31
|
+
'envelope-received',
|
|
32
|
+
'consumed-by-parent',
|
|
33
|
+
'terminalized',
|
|
34
|
+
'lost',
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* `id` regex: ASCII letters, digits, dot, underscore, hyphen; 1-200 chars.
|
|
38
|
+
* Excludes path separators / NUL / whitespace / Unicode (per D1 of the
|
|
39
|
+
* caller-id contract — node ids appear in file paths).
|
|
40
|
+
*/
|
|
41
|
+
export const NODE_ID_REGEX = /^[a-zA-Z0-9._-]{1,200}$/;
|
|
42
|
+
export const WORKFLOW_ID_REGEX = /^[a-zA-Z0-9._-]{1,200}$/;
|
|
43
|
+
export const DISPATCH_REF_REGEX = /^dispatch\/[a-zA-Z0-9._/-]{1,400}\.json$/;
|
|
44
|
+
/** `graphRef` must normalize to `graphs/<workflowId>.json`. */
|
|
45
|
+
export function normalizeGraphRef(graphRef, workflowId) {
|
|
46
|
+
return `graphs/${workflowId}.json`;
|
|
47
|
+
}
|
|
48
|
+
/** Validate `graphRef` does not escape the session directory. */
|
|
49
|
+
export function isSafeRelativeGraphRef(graphRef, workflowId) {
|
|
50
|
+
if (typeof graphRef !== 'string' || graphRef.length === 0)
|
|
51
|
+
return false;
|
|
52
|
+
if (graphRef !== normalizeGraphRef(graphRef, workflowId))
|
|
53
|
+
return false;
|
|
54
|
+
if (graphRef.includes('\\'))
|
|
55
|
+
return false;
|
|
56
|
+
if (graphRef.includes('..'))
|
|
57
|
+
return false;
|
|
58
|
+
if (graphRef.startsWith('/'))
|
|
59
|
+
return false;
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
/** Throws `TypeError` with `code` set on shape failure (used by pure validators). */
|
|
63
|
+
export function failShape(code, message) {
|
|
64
|
+
const err = new TypeError(message);
|
|
65
|
+
err.code = code;
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow in-flight probe (RD §4 D4d — slice 4.0.8).
|
|
3
|
+
*
|
|
4
|
+
* The single source of truth for `inFlightBatch` in 24h mode is the
|
|
5
|
+
* set of `status=running && age(lastHeartbeat)<30min` nodes across
|
|
6
|
+
* all valid graphs for the bound session. No CLI boolean or lease-age
|
|
7
|
+
* heuristic may override graph facts in production. This probe is the
|
|
8
|
+
* production replacement for the previous lease-age `inFlightBatch`
|
|
9
|
+
* computation in `auto-compact-orchestrator.ts`.
|
|
10
|
+
*/
|
|
11
|
+
import { type GraphNodeStatus, type WorkflowId } from './workflow-graph-types.js';
|
|
12
|
+
export declare const FRESH_RUNNING_THRESHOLD_MS: number;
|
|
13
|
+
export declare const PEAKS_HEARTBEAT_MISSING = "PEAKS_HEARTBEAT_MISSING";
|
|
14
|
+
export declare const PEAKS_GRAPH_REF_BROKEN = "PEAKS_GRAPH_REF_BROKEN";
|
|
15
|
+
export declare const PEAKS_GRAPH_CORRUPTED = "PEAKS_GRAPH_CORRUPTED";
|
|
16
|
+
export interface ProbeGraph {
|
|
17
|
+
readonly workflowId?: WorkflowId;
|
|
18
|
+
readonly rootSkill?: string;
|
|
19
|
+
readonly nodes?: ReadonlyArray<{
|
|
20
|
+
readonly id?: string;
|
|
21
|
+
readonly kind?: string;
|
|
22
|
+
readonly status?: GraphNodeStatus;
|
|
23
|
+
readonly lastHeartbeat?: string;
|
|
24
|
+
}>;
|
|
25
|
+
readonly edges?: ReadonlyArray<unknown>;
|
|
26
|
+
readonly schemaVersion?: number;
|
|
27
|
+
/** Sentinel for corrupt graphs the test seam wants to surface as broken. */
|
|
28
|
+
readonly corrupt?: boolean;
|
|
29
|
+
readonly graphRef?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ProbeInput {
|
|
32
|
+
readonly now: string;
|
|
33
|
+
readonly graphs: ReadonlyArray<ProbeGraph>;
|
|
34
|
+
readonly leases?: ReadonlyArray<unknown>;
|
|
35
|
+
readonly redLine?: boolean;
|
|
36
|
+
readonly autoCompact?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface ProbeWarning {
|
|
39
|
+
readonly code: string;
|
|
40
|
+
readonly message: string;
|
|
41
|
+
readonly graphRef?: string;
|
|
42
|
+
readonly nodeId?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ProbeResult {
|
|
45
|
+
readonly inFlightBatch: boolean;
|
|
46
|
+
readonly shouldCompact: boolean;
|
|
47
|
+
readonly deferPreCompact: boolean;
|
|
48
|
+
readonly warnings: ReadonlyArray<ProbeWarning>;
|
|
49
|
+
readonly errors: ReadonlyArray<ProbeWarning>;
|
|
50
|
+
readonly reason: string;
|
|
51
|
+
}
|
|
52
|
+
export declare function probeInFlightBatch(input: ProbeInput): ProbeResult;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow in-flight probe (RD §4 D4d — slice 4.0.8).
|
|
3
|
+
*
|
|
4
|
+
* The single source of truth for `inFlightBatch` in 24h mode is the
|
|
5
|
+
* set of `status=running && age(lastHeartbeat)<30min` nodes across
|
|
6
|
+
* all valid graphs for the bound session. No CLI boolean or lease-age
|
|
7
|
+
* heuristic may override graph facts in production. This probe is the
|
|
8
|
+
* production replacement for the previous lease-age `inFlightBatch`
|
|
9
|
+
* computation in `auto-compact-orchestrator.ts`.
|
|
10
|
+
*/
|
|
11
|
+
export const FRESH_RUNNING_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes
|
|
12
|
+
export const PEAKS_HEARTBEAT_MISSING = 'PEAKS_HEARTBEAT_MISSING';
|
|
13
|
+
export const PEAKS_GRAPH_REF_BROKEN = 'PEAKS_GRAPH_REF_BROKEN';
|
|
14
|
+
export const PEAKS_GRAPH_CORRUPTED = 'PEAKS_GRAPH_CORRUPTED';
|
|
15
|
+
const NON_RUNNING = new Set([
|
|
16
|
+
'prepared',
|
|
17
|
+
'dispatched',
|
|
18
|
+
'envelope-received',
|
|
19
|
+
'consumed-by-parent',
|
|
20
|
+
'terminalized',
|
|
21
|
+
'lost',
|
|
22
|
+
]);
|
|
23
|
+
export function probeInFlightBatch(input) {
|
|
24
|
+
const nowMs = new Date(input.now).getTime();
|
|
25
|
+
const warnings = [];
|
|
26
|
+
const errors = [];
|
|
27
|
+
let inFlight = false;
|
|
28
|
+
for (const graph of input.graphs) {
|
|
29
|
+
if (graph.corrupt === true || graph.schemaVersion !== 1) {
|
|
30
|
+
errors.push({
|
|
31
|
+
code: PEAKS_GRAPH_REF_BROKEN,
|
|
32
|
+
message: 'corrupt or missing graph',
|
|
33
|
+
...(graph.graphRef ? { graphRef: graph.graphRef } : {}),
|
|
34
|
+
});
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!graph.nodes || !Array.isArray(graph.nodes)) {
|
|
38
|
+
errors.push({
|
|
39
|
+
code: PEAKS_GRAPH_CORRUPTED,
|
|
40
|
+
message: 'graph.nodes missing',
|
|
41
|
+
...(graph.graphRef ? { graphRef: graph.graphRef } : {}),
|
|
42
|
+
});
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
for (const node of graph.nodes) {
|
|
46
|
+
if (node.status !== 'running')
|
|
47
|
+
continue;
|
|
48
|
+
if (!node.lastHeartbeat || typeof node.lastHeartbeat !== 'string') {
|
|
49
|
+
warnings.push({
|
|
50
|
+
code: PEAKS_HEARTBEAT_MISSING,
|
|
51
|
+
message: 'running node missing lastHeartbeat',
|
|
52
|
+
...(node.id ? { nodeId: node.id } : {}),
|
|
53
|
+
});
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const hbMs = new Date(node.lastHeartbeat).getTime();
|
|
57
|
+
if (!Number.isFinite(hbMs)) {
|
|
58
|
+
warnings.push({
|
|
59
|
+
code: PEAKS_HEARTBEAT_MISSING,
|
|
60
|
+
message: 'heartbeat unparseable',
|
|
61
|
+
...(node.id ? { nodeId: node.id } : {}),
|
|
62
|
+
});
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const age = nowMs - hbMs;
|
|
66
|
+
if (age < FRESH_RUNNING_THRESHOLD_MS) {
|
|
67
|
+
inFlight = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Red-line compaction overrides in-flight deferral (RD §8 TC-IF-09).
|
|
72
|
+
const shouldCompact = input.redLine === true ? true : !inFlight;
|
|
73
|
+
const deferPreCompact = inFlight;
|
|
74
|
+
return {
|
|
75
|
+
inFlightBatch: inFlight,
|
|
76
|
+
shouldCompact,
|
|
77
|
+
deferPreCompact,
|
|
78
|
+
warnings,
|
|
79
|
+
errors,
|
|
80
|
+
reason: inFlight
|
|
81
|
+
? 'fresh-running-node'
|
|
82
|
+
: (errors.length > 0 ? 'corrupt-graphs' : 'no-fresh-running-node'),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
void NON_RUNNING;
|
|
86
|
+
void {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow node lifecycle — pure transition tables (RD §3).
|
|
3
|
+
*
|
|
4
|
+
* Pure functions: no filesystem, no env access. The transition
|
|
5
|
+
* tables encode the allowed lease + node status transitions defined
|
|
6
|
+
* in RD §3. Forbidden transitions return a typed error code so the
|
|
7
|
+
* CLI / orchestrator can surface actionable hints.
|
|
8
|
+
*
|
|
9
|
+
* Anti-fake-green rule: this module never imports a vendor env var.
|
|
10
|
+
* The vendor signal lives entirely in the IDE adapters.
|
|
11
|
+
*/
|
|
12
|
+
import { type GraphNodeStatus, type LeaseStatus, type AckStatus, type WorkflowGraph, type WorkflowGraphNode, type NodeId, type WorkflowId } from './workflow-graph-types.js';
|
|
13
|
+
export interface LifecycleError extends Error {
|
|
14
|
+
readonly code: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function transitionLease(from: LeaseStatus, to: LeaseStatus, input?: Record<string, unknown>): Record<string, unknown>;
|
|
17
|
+
export declare function transitionNode(from: GraphNodeStatus, to: GraphNodeStatus, input?: Record<string, unknown>): Record<string, unknown>;
|
|
18
|
+
export declare function acknowledgeNode(node: WorkflowGraphNode): WorkflowGraphNode;
|
|
19
|
+
export declare function markNodeLost(node: WorkflowGraphNode, reason: string): WorkflowGraphNode;
|
|
20
|
+
declare function _purePrepareNode(graph: WorkflowGraph, node: WorkflowGraphNode): WorkflowGraph;
|
|
21
|
+
export declare function isFirstHeartbeat(node: WorkflowGraphNode, inputFirstHeartbeat: unknown): boolean;
|
|
22
|
+
export declare function buildGraphNode(input: {
|
|
23
|
+
id: NodeId;
|
|
24
|
+
kind: 'step' | 'dispatch' | 'terminal';
|
|
25
|
+
label: string;
|
|
26
|
+
status: GraphNodeStatus;
|
|
27
|
+
dispatchRef?: string;
|
|
28
|
+
lastHeartbeat?: string;
|
|
29
|
+
ackStatus?: AckStatus;
|
|
30
|
+
dependsOn?: readonly NodeId[];
|
|
31
|
+
}): WorkflowGraphNode;
|
|
32
|
+
export interface GraphDispatchBinding {
|
|
33
|
+
readonly workflowId: WorkflowId;
|
|
34
|
+
readonly graphNodeId: NodeId;
|
|
35
|
+
readonly graphRef: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function resolveDispatchBinding(input: {
|
|
38
|
+
workflowId?: WorkflowId;
|
|
39
|
+
graphNodeId?: NodeId;
|
|
40
|
+
graphRef?: string;
|
|
41
|
+
}): GraphDispatchBinding;
|
|
42
|
+
export interface NodeLifecycleInput {
|
|
43
|
+
readonly projectRoot?: string;
|
|
44
|
+
readonly sessionId?: string;
|
|
45
|
+
readonly workflowId?: string;
|
|
46
|
+
readonly graphRef?: string;
|
|
47
|
+
readonly nodeId?: string;
|
|
48
|
+
readonly graphNode?: WorkflowGraphNode;
|
|
49
|
+
readonly dispatchRef?: string;
|
|
50
|
+
readonly envelopeDispatchRef?: string;
|
|
51
|
+
readonly status?: GraphNodeStatus;
|
|
52
|
+
readonly lastHeartbeat?: string;
|
|
53
|
+
readonly firstHeartbeat?: boolean;
|
|
54
|
+
readonly dependsOn?: readonly NodeId[];
|
|
55
|
+
readonly duplicate?: boolean;
|
|
56
|
+
readonly cycle?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export declare function prepareNodeAction(input: NodeLifecycleInput): WorkflowGraphNode;
|
|
59
|
+
export declare function dispatchNode(input: NodeLifecycleInput): Record<string, unknown>;
|
|
60
|
+
export declare function heartbeatNode(input: NodeLifecycleInput): Record<string, unknown>;
|
|
61
|
+
export declare function writeEnvelope(input: NodeLifecycleInput): Record<string, unknown>;
|
|
62
|
+
export declare function ackNode(input: NodeLifecycleInput): Record<string, unknown>;
|
|
63
|
+
export declare function markLost(input: NodeLifecycleInput): Record<string, unknown>;
|
|
64
|
+
export declare function prepareNode(arg1: WorkflowGraph | NodeLifecycleInput, arg2?: WorkflowGraphNode): WorkflowGraph | WorkflowGraphNode;
|
|
65
|
+
export declare const nodePrepare: typeof prepareNodeAction;
|
|
66
|
+
export declare const nodePreparePure: typeof _purePrepareNode;
|
|
67
|
+
export {};
|