youmd 0.10.2 → 0.12.0
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/dist/commands/brain.d.ts.map +1 -1
- package/dist/commands/brain.js +91 -28
- package/dist/commands/brain.js.map +1 -1
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +6 -1
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/conversations.d.ts +5 -0
- package/dist/commands/conversations.d.ts.map +1 -0
- package/dist/commands/conversations.js +116 -0
- package/dist/commands/conversations.js.map +1 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +16 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/founder.d.ts +13 -0
- package/dist/commands/founder.d.ts.map +1 -0
- package/dist/commands/founder.js +189 -0
- package/dist/commands/founder.js.map +1 -0
- package/dist/commands/handoff.d.ts +5 -0
- package/dist/commands/handoff.d.ts.map +1 -0
- package/dist/commands/handoff.js +432 -0
- package/dist/commands/handoff.js.map +1 -0
- package/dist/commands/orchestrate.d.ts +15 -0
- package/dist/commands/orchestrate.d.ts.map +1 -1
- package/dist/commands/orchestrate.js +294 -0
- package/dist/commands/orchestrate.js.map +1 -1
- package/dist/commands/project.d.ts.map +1 -1
- package/dist/commands/project.js +8 -1
- package/dist/commands/project.js.map +1 -1
- package/dist/commands/reconcile.d.ts +15 -0
- package/dist/commands/reconcile.d.ts.map +1 -0
- package/dist/commands/reconcile.js +260 -0
- package/dist/commands/reconcile.js.map +1 -0
- package/dist/commands/session.d.ts +28 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +895 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/commands/stack.d.ts +2 -0
- package/dist/commands/stack.d.ts.map +1 -1
- package/dist/commands/stack.js +26 -0
- package/dist/commands/stack.js.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +11 -0
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/tasks.d.ts +57 -0
- package/dist/commands/tasks.d.ts.map +1 -1
- package/dist/commands/tasks.js +236 -31
- package/dist/commands/tasks.js.map +1 -1
- package/dist/generated/capability-registry.d.ts +311 -71
- package/dist/generated/capability-registry.d.ts.map +1 -1
- package/dist/generated/capability-registry.js +346 -71
- package/dist/generated/capability-registry.js.map +1 -1
- package/dist/index.js +113 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +362 -1
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +171 -0
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/brain/adapters.d.ts +20 -1
- package/dist/lib/brain/adapters.d.ts.map +1 -1
- package/dist/lib/brain/adapters.js +113 -2
- package/dist/lib/brain/adapters.js.map +1 -1
- package/dist/lib/brain/approval-key-trust.d.ts +29 -0
- package/dist/lib/brain/approval-key-trust.d.ts.map +1 -0
- package/dist/lib/brain/approval-key-trust.js +139 -0
- package/dist/lib/brain/approval-key-trust.js.map +1 -0
- package/dist/lib/brain/cache.d.ts +2 -2
- package/dist/lib/brain/cache.d.ts.map +1 -1
- package/dist/lib/brain/cache.js +4 -4
- package/dist/lib/brain/cache.js.map +1 -1
- package/dist/lib/brain/content.d.ts +56 -0
- package/dist/lib/brain/content.d.ts.map +1 -0
- package/dist/lib/brain/content.js +362 -0
- package/dist/lib/brain/content.js.map +1 -0
- package/dist/lib/brain/descriptor-validator.d.ts +11 -0
- package/dist/lib/brain/descriptor-validator.d.ts.map +1 -0
- package/dist/lib/brain/descriptor-validator.js +185 -0
- package/dist/lib/brain/descriptor-validator.js.map +1 -0
- package/dist/lib/brain/discovery.d.ts.map +1 -1
- package/dist/lib/brain/discovery.js +103 -60
- package/dist/lib/brain/discovery.js.map +1 -1
- package/dist/lib/brain/index.d.ts +2 -0
- package/dist/lib/brain/index.d.ts.map +1 -1
- package/dist/lib/brain/index.js +8 -2
- package/dist/lib/brain/index.js.map +1 -1
- package/dist/lib/brain/proposal.d.ts +53 -5
- package/dist/lib/brain/proposal.d.ts.map +1 -1
- package/dist/lib/brain/proposal.js +341 -64
- package/dist/lib/brain/proposal.js.map +1 -1
- package/dist/lib/brain/safe-file.d.ts +25 -1
- package/dist/lib/brain/safe-file.d.ts.map +1 -1
- package/dist/lib/brain/safe-file.js +250 -30
- package/dist/lib/brain/safe-file.js.map +1 -1
- package/dist/lib/brain/safe-open.d.ts +2 -0
- package/dist/lib/brain/safe-open.d.ts.map +1 -0
- package/dist/lib/brain/safe-open.js +77 -0
- package/dist/lib/brain/safe-open.js.map +1 -0
- package/dist/lib/brain/safety.d.ts.map +1 -1
- package/dist/lib/brain/safety.js +5 -0
- package/dist/lib/brain/safety.js.map +1 -1
- package/dist/lib/brain/staged-apply.d.ts +2 -1
- package/dist/lib/brain/staged-apply.d.ts.map +1 -1
- package/dist/lib/brain/staged-apply.js +6 -3
- package/dist/lib/brain/staged-apply.js.map +1 -1
- package/dist/lib/brain/store.d.ts +3 -0
- package/dist/lib/brain/store.d.ts.map +1 -1
- package/dist/lib/brain/store.js +68 -0
- package/dist/lib/brain/store.js.map +1 -1
- package/dist/lib/brain/sync.d.ts +87 -0
- package/dist/lib/brain/sync.d.ts.map +1 -0
- package/dist/lib/brain/sync.js +238 -0
- package/dist/lib/brain/sync.js.map +1 -0
- package/dist/lib/brain/types.d.ts +23 -0
- package/dist/lib/brain/types.d.ts.map +1 -1
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +3 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/daemon.js +1 -1
- package/dist/lib/daemon.js.map +1 -1
- package/dist/lib/goalRegistryFile.d.ts +169 -0
- package/dist/lib/goalRegistryFile.d.ts.map +1 -0
- package/dist/lib/goalRegistryFile.js +518 -0
- package/dist/lib/goalRegistryFile.js.map +1 -0
- package/dist/lib/goalsFile.d.ts +101 -0
- package/dist/lib/goalsFile.d.ts.map +1 -0
- package/dist/lib/goalsFile.js +413 -0
- package/dist/lib/goalsFile.js.map +1 -0
- package/dist/lib/handoff.d.ts +170 -0
- package/dist/lib/handoff.d.ts.map +1 -0
- package/dist/lib/handoff.js +316 -0
- package/dist/lib/handoff.js.map +1 -0
- package/dist/lib/machine-identity.d.ts +2 -0
- package/dist/lib/machine-identity.d.ts.map +1 -1
- package/dist/lib/machine-identity.js +13 -6
- package/dist/lib/machine-identity.js.map +1 -1
- package/dist/lib/onboarding.d.ts +4 -1
- package/dist/lib/onboarding.d.ts.map +1 -1
- package/dist/lib/onboarding.js +11 -3
- package/dist/lib/onboarding.js.map +1 -1
- package/dist/lib/orchestrator/loop.d.ts +7 -0
- package/dist/lib/orchestrator/loop.d.ts.map +1 -1
- package/dist/lib/orchestrator/loop.js +53 -0
- package/dist/lib/orchestrator/loop.js.map +1 -1
- package/dist/lib/orchestrator/run-journal.d.ts +178 -0
- package/dist/lib/orchestrator/run-journal.d.ts.map +1 -0
- package/dist/lib/orchestrator/run-journal.js +912 -0
- package/dist/lib/orchestrator/run-journal.js.map +1 -0
- package/dist/lib/orchestrator/supervisor.d.ts +11 -0
- package/dist/lib/orchestrator/supervisor.d.ts.map +1 -1
- package/dist/lib/orchestrator/supervisor.js +304 -116
- package/dist/lib/orchestrator/supervisor.js.map +1 -1
- package/dist/lib/orchestrator/tools.d.ts +2 -0
- package/dist/lib/orchestrator/tools.d.ts.map +1 -1
- package/dist/lib/orchestrator/tools.js +214 -1
- package/dist/lib/orchestrator/tools.js.map +1 -1
- package/dist/lib/redact.d.ts.map +1 -1
- package/dist/lib/redact.js +31 -6
- package/dist/lib/redact.js.map +1 -1
- package/dist/lib/remote-agent-event.d.ts +19 -0
- package/dist/lib/remote-agent-event.d.ts.map +1 -0
- package/dist/lib/remote-agent-event.js +85 -0
- package/dist/lib/remote-agent-event.js.map +1 -0
- package/dist/lib/remote-command.d.ts.map +1 -1
- package/dist/lib/remote-command.js +19 -0
- package/dist/lib/remote-command.js.map +1 -1
- package/dist/lib/remote-executor.d.ts +7 -1
- package/dist/lib/remote-executor.d.ts.map +1 -1
- package/dist/lib/remote-executor.js +391 -0
- package/dist/lib/remote-executor.js.map +1 -1
- package/dist/lib/sessionCheckpoint.d.ts +104 -0
- package/dist/lib/sessionCheckpoint.d.ts.map +1 -0
- package/dist/lib/sessionCheckpoint.js +191 -0
- package/dist/lib/sessionCheckpoint.js.map +1 -0
- package/dist/lib/sessionHandoff.d.ts +47 -0
- package/dist/lib/sessionHandoff.d.ts.map +1 -0
- package/dist/lib/sessionHandoff.js +305 -0
- package/dist/lib/sessionHandoff.js.map +1 -0
- package/dist/lib/stream.d.ts +3 -1
- package/dist/lib/stream.d.ts.map +1 -1
- package/dist/lib/stream.js +8 -3
- package/dist/lib/stream.js.map +1 -1
- package/dist/lib/taskLedgerProposal.d.ts +102 -0
- package/dist/lib/taskLedgerProposal.d.ts.map +1 -0
- package/dist/lib/taskLedgerProposal.js +408 -0
- package/dist/lib/taskLedgerProposal.js.map +1 -0
- package/dist/lib/tasksFile.d.ts +23 -2
- package/dist/lib/tasksFile.d.ts.map +1 -1
- package/dist/lib/tasksFile.js +122 -10
- package/dist/lib/tasksFile.js.map +1 -1
- package/dist/mcp/registry.d.ts.map +1 -1
- package/dist/mcp/registry.js +110 -0
- package/dist/mcp/registry.js.map +1 -1
- package/native/prebuilds/darwin-universal/youmd_safe_open.node +0 -0
- package/native/safe-open/youmd_safe_open.c +224 -0
- package/package.json +11 -3
- package/scripts/build-safe-open.mjs +63 -0
- package/scripts/skillstack-sync/com.you.context-sync.plist +1 -1
- package/scripts/skillstack-sync/com.you.identity-sync.plist +1 -1
- package/scripts/skillstack-sync/com.you.orchestrator-watch.plist +1 -1
- package/scripts/skillstack-sync/com.you.realtime-sync.plist +7 -1
- package/scripts/skillstack-sync/com.you.skillstack-sync.plist +1 -1
- package/scripts/skillstack-sync/context-sync.sh +19 -7
- package/scripts/skillstack-sync/install-daemons-linux.sh +6 -0
- package/scripts/smoke-safe-open-node.cjs +50 -0
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// run-journal.ts — durable, atomic state for governed orchestrator runs.
|
|
3
|
+
//
|
|
4
|
+
// A run becomes governed only when spawn_agent carries a run_id. Ungoverned loops keep their
|
|
5
|
+
// historical behavior. Governed runs cannot complete until a distinct, terminal reviewer records
|
|
6
|
+
// a structured approval; rejected reviews advance a bounded retry counter or block the run.
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.MAX_RUN_EVENTS = exports.RUN_EVIDENCE_KINDS = exports.RUN_WORKER_ROLES = void 0;
|
|
42
|
+
exports.isRunWorkerRole = isRunWorkerRole;
|
|
43
|
+
exports.validateAssignmentPreflight = validateAssignmentPreflight;
|
|
44
|
+
exports.loadRunJournal = loadRunJournal;
|
|
45
|
+
exports.saveRunJournal = saveRunJournal;
|
|
46
|
+
exports.createOrLoadRun = createOrLoadRun;
|
|
47
|
+
exports.assignRunWorker = assignRunWorker;
|
|
48
|
+
exports.recordRunWorkerAssertion = recordRunWorkerAssertion;
|
|
49
|
+
exports.completeRunReview = completeRunReview;
|
|
50
|
+
exports.findBoundUnfinishedRunIds = findBoundUnfinishedRunIds;
|
|
51
|
+
exports.runFinishBlocker = runFinishBlocker;
|
|
52
|
+
const fs = __importStar(require("fs"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
const config_1 = require("../config");
|
|
55
|
+
exports.RUN_WORKER_ROLES = ["planner", "specialist", "qa", "design", "reviewer"];
|
|
56
|
+
exports.RUN_EVIDENCE_KINDS = ["test", "artifact", "screenshot", "log", "review", "metric"];
|
|
57
|
+
exports.MAX_RUN_EVENTS = 256;
|
|
58
|
+
const RUN_ID_TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
59
|
+
const MAX_RUN_ID_LENGTH = 128;
|
|
60
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
61
|
+
const DEFAULT_MAX_WORKER_ASSIGNMENTS = 20;
|
|
62
|
+
const DEFAULT_MAX_EVIDENCE_ITEMS = 64;
|
|
63
|
+
const MAX_EVENT_TEXT = 1000;
|
|
64
|
+
const MAX_EVIDENCE_REF = 1000;
|
|
65
|
+
const MAX_RUN_WORKER_IDENTITIES = 512;
|
|
66
|
+
function runsDir(options = {}) {
|
|
67
|
+
return options.dir ?? path.join((0, config_1.getWritableHomeBundleDir)(), "orchestrator", "runs");
|
|
68
|
+
}
|
|
69
|
+
function validRunId(runId) {
|
|
70
|
+
if (!runId || runId.length > MAX_RUN_ID_LENGTH)
|
|
71
|
+
return false;
|
|
72
|
+
const token = runId.startsWith("run:") ? runId.slice(4) : runId;
|
|
73
|
+
return RUN_ID_TOKEN_RE.test(token);
|
|
74
|
+
}
|
|
75
|
+
function isRunWorkerRole(role) {
|
|
76
|
+
return exports.RUN_WORKER_ROLES.includes(role);
|
|
77
|
+
}
|
|
78
|
+
function runPath(runId, options = {}) {
|
|
79
|
+
if (!validRunId(runId))
|
|
80
|
+
throw new Error("run_id must use an optional run: prefix and a bounded safe token");
|
|
81
|
+
return path.join(runsDir(options), `${runId}.json`);
|
|
82
|
+
}
|
|
83
|
+
function isStringRecord(value) {
|
|
84
|
+
return (!!value &&
|
|
85
|
+
typeof value === "object" &&
|
|
86
|
+
!Array.isArray(value) &&
|
|
87
|
+
Object.values(value).every((entry) => typeof entry === "string"));
|
|
88
|
+
}
|
|
89
|
+
function isStringArray(value) {
|
|
90
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string" && !!entry.trim());
|
|
91
|
+
}
|
|
92
|
+
function boundedText(value, max = MAX_EVENT_TEXT) {
|
|
93
|
+
return value
|
|
94
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
95
|
+
.replace(/\s+/g, " ")
|
|
96
|
+
.trim()
|
|
97
|
+
.slice(0, max);
|
|
98
|
+
}
|
|
99
|
+
function isSafeEvidenceRef(value) {
|
|
100
|
+
const ref = value.trim();
|
|
101
|
+
if (!ref || ref.length > MAX_EVIDENCE_REF || /[\u0000-\u001f]/.test(ref))
|
|
102
|
+
return false;
|
|
103
|
+
if (/^https?:\/\//i.test(ref)) {
|
|
104
|
+
try {
|
|
105
|
+
const url = new URL(ref);
|
|
106
|
+
return url.protocol === "https:" || url.protocol === "http:";
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (path.isAbsolute(ref) || ref.split(/[\\/]+/).includes(".."))
|
|
113
|
+
return false;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
function isRunEvidence(value) {
|
|
117
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
118
|
+
return false;
|
|
119
|
+
const row = value;
|
|
120
|
+
return (exports.RUN_EVIDENCE_KINDS.includes(String(row.kind)) &&
|
|
121
|
+
typeof row.source === "string" &&
|
|
122
|
+
!!row.source.trim() &&
|
|
123
|
+
typeof row.workerId === "string" &&
|
|
124
|
+
!!row.workerId.trim() &&
|
|
125
|
+
isRunWorkerRole(String(row.role)) &&
|
|
126
|
+
typeof row.summary === "string" &&
|
|
127
|
+
!!row.summary.trim() &&
|
|
128
|
+
typeof row.recordedAt === "string" &&
|
|
129
|
+
(row.ref === undefined || (typeof row.ref === "string" && isSafeEvidenceRef(row.ref))) &&
|
|
130
|
+
(row.sha256 === undefined || (typeof row.sha256 === "string" && /^[a-f0-9]{64}$/i.test(row.sha256))));
|
|
131
|
+
}
|
|
132
|
+
function isDesignApplicability(value) {
|
|
133
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
134
|
+
return false;
|
|
135
|
+
const row = value;
|
|
136
|
+
return ((row.status === "required" || row.status === "not_applicable") &&
|
|
137
|
+
typeof row.reason === "string" &&
|
|
138
|
+
(row.status === "required" || !!row.reason.trim()));
|
|
139
|
+
}
|
|
140
|
+
function normalizeEvidence(values, workerId, role, recordedAt) {
|
|
141
|
+
const evidence = [];
|
|
142
|
+
for (const value of values) {
|
|
143
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
144
|
+
return { error: "evidence must use typed provenance objects, not free-form strings" };
|
|
145
|
+
}
|
|
146
|
+
const row = value;
|
|
147
|
+
const kind = String(row.kind || "");
|
|
148
|
+
const source = boundedText(String(row.source || ""), 256);
|
|
149
|
+
const summary = boundedText(String(row.summary || ""));
|
|
150
|
+
const ref = row.ref === undefined ? undefined : String(row.ref).trim();
|
|
151
|
+
const sha256 = row.sha256 === undefined ? undefined : String(row.sha256).trim().toLowerCase();
|
|
152
|
+
const candidate = {
|
|
153
|
+
kind,
|
|
154
|
+
source,
|
|
155
|
+
workerId,
|
|
156
|
+
role,
|
|
157
|
+
summary,
|
|
158
|
+
...(ref ? { ref } : {}),
|
|
159
|
+
...(sha256 ? { sha256 } : {}),
|
|
160
|
+
recordedAt,
|
|
161
|
+
};
|
|
162
|
+
if (!isRunEvidence(candidate)) {
|
|
163
|
+
return {
|
|
164
|
+
error: "evidence requires kind=test|artifact|screenshot|log|review|metric, bounded source/summary, a safe relative/http(s) ref, and optional sha256",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
evidence.push(candidate);
|
|
168
|
+
}
|
|
169
|
+
return { evidence };
|
|
170
|
+
}
|
|
171
|
+
function isPreflight(value) {
|
|
172
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
173
|
+
return false;
|
|
174
|
+
const row = value;
|
|
175
|
+
return ((row.mode === "auto" || row.mode === "explicit") &&
|
|
176
|
+
typeof row.harness === "string" &&
|
|
177
|
+
!!row.harness.trim() &&
|
|
178
|
+
typeof row.host === "string" &&
|
|
179
|
+
!!row.host.trim() &&
|
|
180
|
+
typeof row.recordedAt === "string" &&
|
|
181
|
+
(row.capabilityReason === undefined || typeof row.capabilityReason === "string") &&
|
|
182
|
+
(row.proof === undefined || typeof row.proof === "string"));
|
|
183
|
+
}
|
|
184
|
+
function isRunEvent(value) {
|
|
185
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
186
|
+
return false;
|
|
187
|
+
const row = value;
|
|
188
|
+
return (Number.isInteger(row.sequence) &&
|
|
189
|
+
Number(row.sequence) >= 1 &&
|
|
190
|
+
[
|
|
191
|
+
"created",
|
|
192
|
+
"worker_assigned",
|
|
193
|
+
"handoff",
|
|
194
|
+
"review_started",
|
|
195
|
+
"worker_result",
|
|
196
|
+
"review_recorded",
|
|
197
|
+
"retry_required",
|
|
198
|
+
"blocked",
|
|
199
|
+
"completed",
|
|
200
|
+
].includes(String(row.type)) &&
|
|
201
|
+
typeof row.at === "string" &&
|
|
202
|
+
Number.isInteger(row.attempt) &&
|
|
203
|
+
Number(row.attempt) >= 1 &&
|
|
204
|
+
["working", "review", "retry_required", "completed", "blocked"].includes(String(row.toStage)) &&
|
|
205
|
+
(row.fromStage === undefined ||
|
|
206
|
+
["working", "review", "retry_required", "completed", "blocked"].includes(String(row.fromStage))) &&
|
|
207
|
+
(row.role === undefined || isRunWorkerRole(row.role)) &&
|
|
208
|
+
(row.workerId === undefined || typeof row.workerId === "string") &&
|
|
209
|
+
(row.from === undefined || typeof row.from === "string") &&
|
|
210
|
+
(row.to === undefined || typeof row.to === "string") &&
|
|
211
|
+
(row.summary === undefined || typeof row.summary === "string") &&
|
|
212
|
+
(row.preflight === undefined || isPreflight(row.preflight)));
|
|
213
|
+
}
|
|
214
|
+
function appendRunEvent(journal, input) {
|
|
215
|
+
const events = Array.isArray(journal.events) ? journal.events : [];
|
|
216
|
+
const sequence = events.reduce((max, event) => Math.max(max, event.sequence), 0) + 1;
|
|
217
|
+
const event = {
|
|
218
|
+
...input,
|
|
219
|
+
sequence,
|
|
220
|
+
attempt: input.attempt ?? journal.attempt,
|
|
221
|
+
...(input.summary ? { summary: boundedText(input.summary) } : {}),
|
|
222
|
+
};
|
|
223
|
+
journal.events = [...events, event].slice(-exports.MAX_RUN_EVENTS);
|
|
224
|
+
}
|
|
225
|
+
function validateAssignmentPreflight(input) {
|
|
226
|
+
if (!input)
|
|
227
|
+
return "governed worker assignment requires capability preflight evidence";
|
|
228
|
+
if (input.mode !== "auto" && input.mode !== "explicit") {
|
|
229
|
+
return "capability preflight mode must be auto or explicit";
|
|
230
|
+
}
|
|
231
|
+
const harness = boundedText(input.harness || "", 128);
|
|
232
|
+
const host = boundedText(input.host || "", 255);
|
|
233
|
+
if (!harness || !host)
|
|
234
|
+
return "capability preflight requires selected harness and host";
|
|
235
|
+
if (input.mode === "auto" && !boundedText(input.capabilityReason || "")) {
|
|
236
|
+
return "auto-selected harness requires a capability reason";
|
|
237
|
+
}
|
|
238
|
+
if (input.mode === "explicit" && !boundedText(input.proof || "")) {
|
|
239
|
+
return "explicit harness selection requires harness proof";
|
|
240
|
+
}
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
function isJournal(value) {
|
|
244
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
245
|
+
return false;
|
|
246
|
+
const row = value;
|
|
247
|
+
const evidenceValid = Array.isArray(row.evidence) && row.evidence.every((item) => {
|
|
248
|
+
if (isRunEvidence(item))
|
|
249
|
+
return true;
|
|
250
|
+
// Read-only compatibility for v1 journals; loadRunJournal upgrades these before mutation.
|
|
251
|
+
return (!!item &&
|
|
252
|
+
typeof item === "object" &&
|
|
253
|
+
typeof item.workerId === "string" &&
|
|
254
|
+
typeof item.role === "string" &&
|
|
255
|
+
typeof item.summary === "string" &&
|
|
256
|
+
typeof item.recordedAt === "string");
|
|
257
|
+
});
|
|
258
|
+
const verdictValid = row.reviewerVerdict === undefined ||
|
|
259
|
+
(!!row.reviewerVerdict &&
|
|
260
|
+
["approved", "changes_requested"].includes(row.reviewerVerdict.status) &&
|
|
261
|
+
typeof row.reviewerVerdict.reviewerId === "string" &&
|
|
262
|
+
typeof row.reviewerVerdict.summary === "string" &&
|
|
263
|
+
Array.isArray(row.reviewerVerdict.issues) &&
|
|
264
|
+
row.reviewerVerdict.issues.every((issue) => typeof issue === "string") &&
|
|
265
|
+
(row.reviewerVerdict.semanticSuccess === undefined ||
|
|
266
|
+
(typeof row.reviewerVerdict.semanticSuccess.met === "boolean" &&
|
|
267
|
+
typeof row.reviewerVerdict.semanticSuccess.summary === "string")) &&
|
|
268
|
+
typeof row.reviewerVerdict.recordedAt === "string");
|
|
269
|
+
const eventsValid = row.events === undefined ||
|
|
270
|
+
(Array.isArray(row.events) && row.events.length <= exports.MAX_RUN_EVENTS && row.events.every(isRunEvent));
|
|
271
|
+
return (typeof row.runId === "string" &&
|
|
272
|
+
validRunId(row.runId) &&
|
|
273
|
+
["working", "review", "retry_required", "completed", "blocked"].includes(String(row.stage)) &&
|
|
274
|
+
isStringRecord(row.workers) &&
|
|
275
|
+
Number.isInteger(row.attempt) &&
|
|
276
|
+
Number(row.attempt) >= 1 &&
|
|
277
|
+
Number.isInteger(row.maxAttempts) &&
|
|
278
|
+
Number(row.maxAttempts) >= 1 &&
|
|
279
|
+
evidenceValid &&
|
|
280
|
+
eventsValid &&
|
|
281
|
+
verdictValid &&
|
|
282
|
+
(row.workflowVersion === undefined || row.workflowVersion === 2) &&
|
|
283
|
+
(row.designApplicability === undefined || isDesignApplicability(row.designApplicability)) &&
|
|
284
|
+
(row.cursor === undefined || [...exports.RUN_WORKER_ROLES, "done", "blocked"].includes(row.cursor)) &&
|
|
285
|
+
(row.workerAssertions === undefined ||
|
|
286
|
+
(Array.isArray(row.workerAssertions) &&
|
|
287
|
+
row.workerAssertions.every((assertion) => !!assertion &&
|
|
288
|
+
Number.isInteger(assertion.attempt) &&
|
|
289
|
+
assertion.attempt >= 1 &&
|
|
290
|
+
typeof assertion.workerId === "string" &&
|
|
291
|
+
["planner", "specialist", "qa", "design"].includes(String(assertion.role)) &&
|
|
292
|
+
(assertion.status === "passed" || assertion.status === "failed") &&
|
|
293
|
+
typeof assertion.summary === "string" &&
|
|
294
|
+
Array.isArray(assertion.evidence) &&
|
|
295
|
+
assertion.evidence.every(isRunEvidence) &&
|
|
296
|
+
typeof assertion.recordedAt === "string"))) &&
|
|
297
|
+
(row.budget === undefined ||
|
|
298
|
+
(Number.isInteger(row.budget.maxAttempts) &&
|
|
299
|
+
row.budget.maxAttempts >= 1 &&
|
|
300
|
+
Number.isInteger(row.budget.maxWorkerAssignments) &&
|
|
301
|
+
row.budget.maxWorkerAssignments >= 1 &&
|
|
302
|
+
Number.isInteger(row.budget.maxEvidenceItems) &&
|
|
303
|
+
row.budget.maxEvidenceItems >= 1)) &&
|
|
304
|
+
(row.usage === undefined ||
|
|
305
|
+
(Number.isInteger(row.usage.workerAssignments) &&
|
|
306
|
+
row.usage.workerAssignments >= 0 &&
|
|
307
|
+
Number.isInteger(row.usage.evidenceItems) &&
|
|
308
|
+
row.usage.evidenceItems >= 0)) &&
|
|
309
|
+
(row.taskId === undefined || typeof row.taskId === "string") &&
|
|
310
|
+
(row.sessionId === undefined || typeof row.sessionId === "string") &&
|
|
311
|
+
(row.executionWorkerIds === undefined ||
|
|
312
|
+
(isStringArray(row.executionWorkerIds) && row.executionWorkerIds.length <= MAX_RUN_WORKER_IDENTITIES)) &&
|
|
313
|
+
(row.reviewerWorkerIds === undefined ||
|
|
314
|
+
(isStringArray(row.reviewerWorkerIds) && row.reviewerWorkerIds.length <= MAX_RUN_WORKER_IDENTITIES)) &&
|
|
315
|
+
typeof row.createdAt === "string" &&
|
|
316
|
+
typeof row.updatedAt === "string");
|
|
317
|
+
}
|
|
318
|
+
function archiveCorrupt(target) {
|
|
319
|
+
try {
|
|
320
|
+
const archived = `${target}.corrupt-${Date.now()}`;
|
|
321
|
+
fs.renameSync(target, archived);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// A concurrent reader may already have archived it. A corrupt journal must never crash U.
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/** Load a persisted run. Invalid/partial JSON is quarantined and treated as missing. */
|
|
328
|
+
function loadRunJournal(runId, options = {}) {
|
|
329
|
+
let target;
|
|
330
|
+
try {
|
|
331
|
+
target = runPath(runId, options);
|
|
332
|
+
const parsed = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
333
|
+
if (!isJournal(parsed) || parsed.runId !== runId) {
|
|
334
|
+
archiveCorrupt(target);
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
const journal = parsed;
|
|
338
|
+
// v1 journals predate append-only events. They remain readable and gain events on the next mutation.
|
|
339
|
+
journal.events = Array.isArray(journal.events) ? journal.events : [];
|
|
340
|
+
journal.evidence = journal.evidence.map((item) => isRunEvidence(item)
|
|
341
|
+
? item
|
|
342
|
+
: (() => {
|
|
343
|
+
const legacy = item;
|
|
344
|
+
return {
|
|
345
|
+
kind: "log",
|
|
346
|
+
source: `legacy:${boundedText(legacy.workerId || "unknown", 128)}`,
|
|
347
|
+
workerId: boundedText(legacy.workerId || "unknown", 128),
|
|
348
|
+
role: isRunWorkerRole(legacy.role || "") ? legacy.role : "reviewer",
|
|
349
|
+
summary: boundedText(legacy.summary || "legacy evidence"),
|
|
350
|
+
recordedAt: legacy.recordedAt || new Date(0).toISOString(),
|
|
351
|
+
};
|
|
352
|
+
})());
|
|
353
|
+
if (journal.reviewerVerdict && !journal.reviewerVerdict.semanticSuccess) {
|
|
354
|
+
journal.reviewerVerdict.semanticSuccess = {
|
|
355
|
+
met: journal.reviewerVerdict.status === "approved",
|
|
356
|
+
summary: "legacy reviewer verdict (semantic-success field was not recorded)",
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
if (journal.workflowVersion === 2) {
|
|
360
|
+
journal.workerAssertions = Array.isArray(journal.workerAssertions) ? journal.workerAssertions : [];
|
|
361
|
+
journal.budget = journal.budget ?? {
|
|
362
|
+
maxAttempts: journal.maxAttempts,
|
|
363
|
+
maxWorkerAssignments: DEFAULT_MAX_WORKER_ASSIGNMENTS,
|
|
364
|
+
maxEvidenceItems: DEFAULT_MAX_EVIDENCE_ITEMS,
|
|
365
|
+
};
|
|
366
|
+
journal.usage = journal.usage ?? {
|
|
367
|
+
workerAssignments: journal.events.filter((event) => event.type === "worker_assigned").length,
|
|
368
|
+
evidenceItems: journal.evidence.length,
|
|
369
|
+
};
|
|
370
|
+
journal.cursor = journal.cursor ?? (journal.stage === "completed" ? "done" : "planner");
|
|
371
|
+
}
|
|
372
|
+
// Older journals only have mutable role slots. Seed the durable identity sets from every
|
|
373
|
+
// retained assignment event plus current slots; future reassignments can no longer erase them.
|
|
374
|
+
const assignedEvents = journal.events.filter((event) => event.type === "worker_assigned");
|
|
375
|
+
journal.executionWorkerIds = Array.from(new Set([
|
|
376
|
+
...(Array.isArray(journal.executionWorkerIds) ? journal.executionWorkerIds : []),
|
|
377
|
+
...assignedEvents
|
|
378
|
+
.filter((event) => event.role !== "reviewer")
|
|
379
|
+
.map((event) => event.workerId)
|
|
380
|
+
.filter((id) => !!id),
|
|
381
|
+
...Object.entries(journal.workers)
|
|
382
|
+
.filter(([role]) => role !== "reviewer")
|
|
383
|
+
.map(([, id]) => id),
|
|
384
|
+
]));
|
|
385
|
+
journal.reviewerWorkerIds = Array.from(new Set([
|
|
386
|
+
...(Array.isArray(journal.reviewerWorkerIds) ? journal.reviewerWorkerIds : []),
|
|
387
|
+
...assignedEvents
|
|
388
|
+
.filter((event) => event.role === "reviewer")
|
|
389
|
+
.map((event) => event.workerId)
|
|
390
|
+
.filter((id) => !!id),
|
|
391
|
+
...(journal.workers.reviewer ? [journal.workers.reviewer] : []),
|
|
392
|
+
]));
|
|
393
|
+
return journal;
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
if (error.code !== "ENOENT" && target)
|
|
397
|
+
archiveCorrupt(target);
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/** Atomic 0600 checkpoint: a restart sees either the old complete record or the new one. */
|
|
402
|
+
function saveRunJournal(journal, options = {}) {
|
|
403
|
+
const target = runPath(journal.runId, options);
|
|
404
|
+
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
405
|
+
const tmp = `${target}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
406
|
+
try {
|
|
407
|
+
fs.writeFileSync(tmp, `${JSON.stringify(journal, null, 2)}\n`, { mode: 0o600 });
|
|
408
|
+
fs.renameSync(tmp, target);
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
try {
|
|
412
|
+
fs.rmSync(tmp, { force: true });
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
/* rename already removed it */
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function createOrLoadRun(input, options = {}) {
|
|
420
|
+
if (!validRunId(input.runId))
|
|
421
|
+
return { ok: false, error: "invalid run_id" };
|
|
422
|
+
const existing = loadRunJournal(input.runId, options);
|
|
423
|
+
if (existing) {
|
|
424
|
+
const taskId = input.taskId?.trim();
|
|
425
|
+
const sessionId = input.sessionId ? boundedText(input.sessionId, 256) : undefined;
|
|
426
|
+
if (taskId && existing.taskId && existing.taskId !== taskId) {
|
|
427
|
+
return { ok: false, error: `run ${input.runId} already belongs to task ${existing.taskId}` };
|
|
428
|
+
}
|
|
429
|
+
if (sessionId && existing.sessionId && existing.sessionId !== sessionId) {
|
|
430
|
+
return { ok: false, error: `run ${input.runId} already belongs to session ${existing.sessionId}` };
|
|
431
|
+
}
|
|
432
|
+
let changed = false;
|
|
433
|
+
if (taskId && !existing.taskId) {
|
|
434
|
+
existing.taskId = taskId;
|
|
435
|
+
changed = true;
|
|
436
|
+
}
|
|
437
|
+
if (sessionId && !existing.sessionId) {
|
|
438
|
+
existing.sessionId = sessionId;
|
|
439
|
+
changed = true;
|
|
440
|
+
}
|
|
441
|
+
if (changed) {
|
|
442
|
+
existing.updatedAt = (options.now?.() ?? new Date()).toISOString();
|
|
443
|
+
saveRunJournal(existing, options);
|
|
444
|
+
}
|
|
445
|
+
return { ok: true, journal: existing };
|
|
446
|
+
}
|
|
447
|
+
if (input.workflowVersion === 2 && !isDesignApplicability(input.designApplicability)) {
|
|
448
|
+
return {
|
|
449
|
+
ok: false,
|
|
450
|
+
error: "strict governed runs require designApplicability.status=required|not_applicable; not_applicable also requires a reason",
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const requestedMax = Number.isFinite(input.maxAttempts) ? Number(input.maxAttempts) : DEFAULT_MAX_ATTEMPTS;
|
|
454
|
+
const maxAttempts = Math.max(1, Math.min(10, Math.trunc(requestedMax)));
|
|
455
|
+
const maxWorkerAssignments = Math.max(4, Math.min(100, Math.trunc(Number(input.maxWorkerAssignments) || DEFAULT_MAX_WORKER_ASSIGNMENTS)));
|
|
456
|
+
const maxEvidenceItems = Math.max(4, Math.min(256, Math.trunc(Number(input.maxEvidenceItems) || DEFAULT_MAX_EVIDENCE_ITEMS)));
|
|
457
|
+
const timestamp = (options.now?.() ?? new Date()).toISOString();
|
|
458
|
+
const journal = {
|
|
459
|
+
runId: input.runId,
|
|
460
|
+
taskId: input.taskId?.trim() || undefined,
|
|
461
|
+
sessionId: input.sessionId ? boundedText(input.sessionId, 256) : undefined,
|
|
462
|
+
stage: "working",
|
|
463
|
+
...(input.workflowVersion === 2
|
|
464
|
+
? {
|
|
465
|
+
workflowVersion: 2,
|
|
466
|
+
designApplicability: {
|
|
467
|
+
status: input.designApplicability.status,
|
|
468
|
+
reason: boundedText(input.designApplicability.reason),
|
|
469
|
+
},
|
|
470
|
+
cursor: "planner",
|
|
471
|
+
workerAssertions: [],
|
|
472
|
+
budget: { maxAttempts, maxWorkerAssignments, maxEvidenceItems },
|
|
473
|
+
usage: { workerAssignments: 0, evidenceItems: 0 },
|
|
474
|
+
}
|
|
475
|
+
: {}),
|
|
476
|
+
workers: {},
|
|
477
|
+
executionWorkerIds: [],
|
|
478
|
+
reviewerWorkerIds: [],
|
|
479
|
+
attempt: 1,
|
|
480
|
+
maxAttempts,
|
|
481
|
+
evidence: [],
|
|
482
|
+
events: [],
|
|
483
|
+
createdAt: timestamp,
|
|
484
|
+
updatedAt: timestamp,
|
|
485
|
+
};
|
|
486
|
+
appendRunEvent(journal, {
|
|
487
|
+
type: "created",
|
|
488
|
+
at: timestamp,
|
|
489
|
+
toStage: "working",
|
|
490
|
+
summary: input.taskId ? `created for task ${input.taskId}` : "created governed run",
|
|
491
|
+
});
|
|
492
|
+
saveRunJournal(journal, options);
|
|
493
|
+
return { ok: true, journal };
|
|
494
|
+
}
|
|
495
|
+
/** Attribute a spawned worker to a durable run and advance it into work or review. */
|
|
496
|
+
function assignRunWorker(input, options = {}) {
|
|
497
|
+
const role = input.role.trim().toLowerCase();
|
|
498
|
+
const workerId = input.workerId.trim();
|
|
499
|
+
if (!role || !workerId)
|
|
500
|
+
return { ok: false, error: "role and workerId are required" };
|
|
501
|
+
if (!isRunWorkerRole(role)) {
|
|
502
|
+
return { ok: false, error: `role must be one of: ${exports.RUN_WORKER_ROLES.join(", ")}` };
|
|
503
|
+
}
|
|
504
|
+
const preflightError = validateAssignmentPreflight(input.preflight);
|
|
505
|
+
if (preflightError)
|
|
506
|
+
return { ok: false, error: preflightError };
|
|
507
|
+
const before = loadRunJournal(input.runId, options);
|
|
508
|
+
if (!before && input.workflowVersion === 2 && role !== "planner") {
|
|
509
|
+
return { ok: false, error: "strict governed runs must start with role=planner" };
|
|
510
|
+
}
|
|
511
|
+
const created = createOrLoadRun(input, options);
|
|
512
|
+
if (!created.ok || !created.journal)
|
|
513
|
+
return created;
|
|
514
|
+
const journal = created.journal;
|
|
515
|
+
if (journal.stage === "completed" || journal.stage === "blocked") {
|
|
516
|
+
return { ok: false, error: `run ${journal.runId} is already ${journal.stage}` };
|
|
517
|
+
}
|
|
518
|
+
if (journal.workflowVersion === 2) {
|
|
519
|
+
if (!journal.cursor || !journal.budget || !journal.usage || !journal.designApplicability) {
|
|
520
|
+
return { ok: false, error: `run ${journal.runId} is missing strict workflow state` };
|
|
521
|
+
}
|
|
522
|
+
if (journal.cursor !== role) {
|
|
523
|
+
return {
|
|
524
|
+
ok: false,
|
|
525
|
+
error: `run ${journal.runId} expects role=${journal.cursor}; cannot assign ${role}`,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
if (journal.workers[role]) {
|
|
529
|
+
return { ok: false, error: `role=${role} already has an assigned worker for attempt ${journal.attempt}` };
|
|
530
|
+
}
|
|
531
|
+
if (journal.usage.workerAssignments >= journal.budget.maxWorkerAssignments) {
|
|
532
|
+
return { ok: false, error: "run worker-assignment budget exhausted" };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const fromStage = journal.stage;
|
|
536
|
+
const timestamp = (options.now?.() ?? new Date()).toISOString();
|
|
537
|
+
const preflight = {
|
|
538
|
+
mode: input.preflight.mode,
|
|
539
|
+
harness: boundedText(input.preflight.harness, 128),
|
|
540
|
+
host: boundedText(input.preflight.host, 255),
|
|
541
|
+
...(input.preflight.capabilityReason
|
|
542
|
+
? { capabilityReason: boundedText(input.preflight.capabilityReason) }
|
|
543
|
+
: {}),
|
|
544
|
+
...(input.preflight.proof ? { proof: boundedText(input.preflight.proof) } : {}),
|
|
545
|
+
recordedAt: timestamp,
|
|
546
|
+
};
|
|
547
|
+
if (role === "reviewer") {
|
|
548
|
+
if (journal.executionWorkerIds.includes(workerId)) {
|
|
549
|
+
return { ok: false, error: "reviewer must be distinct from every specialist worker" };
|
|
550
|
+
}
|
|
551
|
+
if (!journal.reviewerWorkerIds.includes(workerId) && journal.reviewerWorkerIds.length >= MAX_RUN_WORKER_IDENTITIES) {
|
|
552
|
+
return { ok: false, error: "run reviewer identity budget exhausted" };
|
|
553
|
+
}
|
|
554
|
+
journal.stage = "review";
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
if (journal.reviewerWorkerIds.includes(workerId)) {
|
|
558
|
+
return { ok: false, error: "specialist cannot reuse the reviewer worker" };
|
|
559
|
+
}
|
|
560
|
+
if (!journal.executionWorkerIds.includes(workerId) && journal.executionWorkerIds.length >= MAX_RUN_WORKER_IDENTITIES) {
|
|
561
|
+
return { ok: false, error: "run execution identity budget exhausted" };
|
|
562
|
+
}
|
|
563
|
+
journal.stage = "working";
|
|
564
|
+
}
|
|
565
|
+
journal.workers[role] = workerId;
|
|
566
|
+
if (role === "reviewer") {
|
|
567
|
+
if (!journal.reviewerWorkerIds.includes(workerId))
|
|
568
|
+
journal.reviewerWorkerIds.push(workerId);
|
|
569
|
+
}
|
|
570
|
+
else if (!journal.executionWorkerIds.includes(workerId)) {
|
|
571
|
+
journal.executionWorkerIds.push(workerId);
|
|
572
|
+
}
|
|
573
|
+
if (journal.workflowVersion === 2 && journal.usage)
|
|
574
|
+
journal.usage.workerAssignments += 1;
|
|
575
|
+
appendRunEvent(journal, {
|
|
576
|
+
type: "worker_assigned",
|
|
577
|
+
at: timestamp,
|
|
578
|
+
fromStage,
|
|
579
|
+
toStage: journal.stage,
|
|
580
|
+
role,
|
|
581
|
+
workerId,
|
|
582
|
+
summary: `${role} assigned with ${preflight.harness} on ${preflight.host}`,
|
|
583
|
+
preflight,
|
|
584
|
+
});
|
|
585
|
+
appendRunEvent(journal, {
|
|
586
|
+
type: "handoff",
|
|
587
|
+
at: timestamp,
|
|
588
|
+
fromStage,
|
|
589
|
+
toStage: journal.stage,
|
|
590
|
+
role,
|
|
591
|
+
workerId,
|
|
592
|
+
from: "orchestrator",
|
|
593
|
+
to: workerId,
|
|
594
|
+
summary: `handoff to ${role}`,
|
|
595
|
+
});
|
|
596
|
+
if (role === "reviewer") {
|
|
597
|
+
appendRunEvent(journal, {
|
|
598
|
+
type: "review_started",
|
|
599
|
+
at: timestamp,
|
|
600
|
+
fromStage,
|
|
601
|
+
toStage: "review",
|
|
602
|
+
role,
|
|
603
|
+
workerId,
|
|
604
|
+
summary: "distinct reviewer assigned",
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
journal.updatedAt = timestamp;
|
|
608
|
+
saveRunJournal(journal, options);
|
|
609
|
+
return { ok: true, journal };
|
|
610
|
+
}
|
|
611
|
+
function requiredExecutionRoles(journal) {
|
|
612
|
+
return journal.designApplicability?.status === "required"
|
|
613
|
+
? ["planner", "specialist", "qa", "design"]
|
|
614
|
+
: ["planner", "specialist", "qa"];
|
|
615
|
+
}
|
|
616
|
+
function moveToRetryOrBlocked(journal, timestamp, summary, workerId, role) {
|
|
617
|
+
const fromStage = journal.stage;
|
|
618
|
+
if (journal.attempt < journal.maxAttempts) {
|
|
619
|
+
journal.attempt += 1;
|
|
620
|
+
journal.stage = "retry_required";
|
|
621
|
+
journal.cursor = "planner";
|
|
622
|
+
journal.workers = {};
|
|
623
|
+
appendRunEvent(journal, {
|
|
624
|
+
type: "retry_required",
|
|
625
|
+
at: timestamp,
|
|
626
|
+
attempt: journal.attempt,
|
|
627
|
+
fromStage,
|
|
628
|
+
toStage: "retry_required",
|
|
629
|
+
role,
|
|
630
|
+
workerId,
|
|
631
|
+
summary,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
journal.stage = "blocked";
|
|
636
|
+
journal.cursor = "blocked";
|
|
637
|
+
journal.blockedAt = timestamp;
|
|
638
|
+
appendRunEvent(journal, {
|
|
639
|
+
type: "blocked",
|
|
640
|
+
at: timestamp,
|
|
641
|
+
fromStage,
|
|
642
|
+
toStage: "blocked",
|
|
643
|
+
role,
|
|
644
|
+
workerId,
|
|
645
|
+
summary,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
/** Record a terminal planner/specialist/QA/design assertion and advance the fixed v2 cursor. */
|
|
650
|
+
function recordRunWorkerAssertion(input, options = {}) {
|
|
651
|
+
const journal = loadRunJournal(input.runId, options);
|
|
652
|
+
if (!journal)
|
|
653
|
+
return { ok: false, error: `run ${input.runId} was not found or is corrupt` };
|
|
654
|
+
if (journal.workflowVersion !== 2 || !journal.cursor || !journal.budget || !journal.usage) {
|
|
655
|
+
return { ok: false, error: "worker assertions require a strict v2 governed run" };
|
|
656
|
+
}
|
|
657
|
+
if (journal.stage === "completed" || journal.stage === "blocked") {
|
|
658
|
+
return { ok: false, error: `run ${journal.runId} is already ${journal.stage}` };
|
|
659
|
+
}
|
|
660
|
+
const role = input.role.trim().toLowerCase();
|
|
661
|
+
if (!isRunWorkerRole(role) || role === "reviewer") {
|
|
662
|
+
return { ok: false, error: "worker result role must be planner, specialist, qa, or design" };
|
|
663
|
+
}
|
|
664
|
+
if (journal.cursor !== role || journal.workers[role] !== input.workerId) {
|
|
665
|
+
return { ok: false, error: `worker result must match the current role=${journal.cursor} assignment` };
|
|
666
|
+
}
|
|
667
|
+
if (!input.workerTerminal)
|
|
668
|
+
return { ok: false, error: "worker must be terminal before recording its result" };
|
|
669
|
+
if (input.status === "passed" && !input.workerSucceeded) {
|
|
670
|
+
return { ok: false, error: "a stopped, failed, or non-zero worker cannot assert passed" };
|
|
671
|
+
}
|
|
672
|
+
const summary = boundedText(input.summary || "");
|
|
673
|
+
if (!summary)
|
|
674
|
+
return { ok: false, error: "worker result requires a non-empty summary" };
|
|
675
|
+
const timestamp = (options.now?.() ?? new Date()).toISOString();
|
|
676
|
+
const normalized = normalizeEvidence(input.evidence ?? [], input.workerId, role, timestamp);
|
|
677
|
+
if (normalized.error)
|
|
678
|
+
return { ok: false, error: normalized.error };
|
|
679
|
+
const evidence = normalized.evidence ?? [];
|
|
680
|
+
if (input.status === "passed" && evidence.length === 0) {
|
|
681
|
+
return { ok: false, error: "passed worker result requires typed evidence" };
|
|
682
|
+
}
|
|
683
|
+
if (journal.usage.evidenceItems + evidence.length > journal.budget.maxEvidenceItems) {
|
|
684
|
+
return { ok: false, error: "run evidence budget exhausted" };
|
|
685
|
+
}
|
|
686
|
+
const assertion = {
|
|
687
|
+
attempt: journal.attempt,
|
|
688
|
+
workerId: input.workerId,
|
|
689
|
+
role,
|
|
690
|
+
status: input.status,
|
|
691
|
+
summary,
|
|
692
|
+
evidence,
|
|
693
|
+
recordedAt: timestamp,
|
|
694
|
+
};
|
|
695
|
+
journal.workerAssertions = [...(journal.workerAssertions ?? []), assertion];
|
|
696
|
+
journal.evidence.push(...evidence);
|
|
697
|
+
journal.usage.evidenceItems += evidence.length;
|
|
698
|
+
appendRunEvent(journal, {
|
|
699
|
+
type: "worker_result",
|
|
700
|
+
at: timestamp,
|
|
701
|
+
fromStage: journal.stage,
|
|
702
|
+
toStage: journal.stage,
|
|
703
|
+
role,
|
|
704
|
+
workerId: input.workerId,
|
|
705
|
+
summary: `${input.status}: ${summary}`,
|
|
706
|
+
});
|
|
707
|
+
if (input.status === "failed") {
|
|
708
|
+
moveToRetryOrBlocked(journal, timestamp, summary, input.workerId, role);
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
const roles = requiredExecutionRoles(journal);
|
|
712
|
+
const currentIndex = roles.indexOf(role);
|
|
713
|
+
journal.cursor = currentIndex === roles.length - 1 ? "reviewer" : roles[currentIndex + 1];
|
|
714
|
+
delete journal.workers[role];
|
|
715
|
+
}
|
|
716
|
+
journal.updatedAt = timestamp;
|
|
717
|
+
saveRunJournal(journal, options);
|
|
718
|
+
return { ok: true, journal };
|
|
719
|
+
}
|
|
720
|
+
/** Apply a terminal review. Approval completes; rejection retries or blocks at the budget. */
|
|
721
|
+
function completeRunReview(input, options = {}) {
|
|
722
|
+
const journal = loadRunJournal(input.runId, options);
|
|
723
|
+
if (!journal)
|
|
724
|
+
return { ok: false, error: `run ${input.runId} was not found or is corrupt` };
|
|
725
|
+
if (journal.stage === "completed" || journal.stage === "blocked") {
|
|
726
|
+
return { ok: false, error: `run ${journal.runId} is already ${journal.stage}` };
|
|
727
|
+
}
|
|
728
|
+
if (journal.workers.reviewer !== input.reviewerId) {
|
|
729
|
+
return { ok: false, error: "reviewer_id must match the worker assigned to role=reviewer" };
|
|
730
|
+
}
|
|
731
|
+
if (journal.executionWorkerIds.includes(input.reviewerId)) {
|
|
732
|
+
return { ok: false, error: "specialist/self-approval is not allowed" };
|
|
733
|
+
}
|
|
734
|
+
if (!input.reviewerTerminal) {
|
|
735
|
+
return { ok: false, error: "reviewer must be terminal before complete_run" };
|
|
736
|
+
}
|
|
737
|
+
if (journal.workflowVersion === 2 && !input.reviewerSucceeded) {
|
|
738
|
+
return { ok: false, error: "reviewer must exit successfully; stopped, failed, and non-zero reviewers cannot approve" };
|
|
739
|
+
}
|
|
740
|
+
const summary = input.verdict.summary?.trim();
|
|
741
|
+
if (!summary || !["approved", "changes_requested"].includes(input.verdict.status)) {
|
|
742
|
+
return { ok: false, error: "verdict requires status=approved|changes_requested and a non-empty summary" };
|
|
743
|
+
}
|
|
744
|
+
const timestamp = (options.now?.() ?? new Date()).toISOString();
|
|
745
|
+
let concreteEvidence;
|
|
746
|
+
if (journal.workflowVersion === 2) {
|
|
747
|
+
if (journal.cursor !== "reviewer" || journal.stage !== "review") {
|
|
748
|
+
return { ok: false, error: `run ${journal.runId} is not ready for review (cursor=${journal.cursor})` };
|
|
749
|
+
}
|
|
750
|
+
const passedRoles = new Set((journal.workerAssertions ?? [])
|
|
751
|
+
.filter((assertion) => assertion.attempt === journal.attempt && assertion.status === "passed")
|
|
752
|
+
.map((assertion) => assertion.role));
|
|
753
|
+
const missing = requiredExecutionRoles(journal).filter((role) => !passedRoles.has(role));
|
|
754
|
+
if (missing.length)
|
|
755
|
+
return { ok: false, error: `review cannot complete; successful assertions missing: ${missing.join(", ")}` };
|
|
756
|
+
const normalized = normalizeEvidence(input.evidence ?? [], input.reviewerId, "reviewer", timestamp);
|
|
757
|
+
if (normalized.error)
|
|
758
|
+
return { ok: false, error: normalized.error };
|
|
759
|
+
concreteEvidence = normalized.evidence ?? [];
|
|
760
|
+
if (!journal.budget || !journal.usage)
|
|
761
|
+
return { ok: false, error: "run is missing budget state" };
|
|
762
|
+
if (journal.usage.evidenceItems + concreteEvidence.length > journal.budget.maxEvidenceItems) {
|
|
763
|
+
return { ok: false, error: "run evidence budget exhausted" };
|
|
764
|
+
}
|
|
765
|
+
const semantic = input.verdict.semanticSuccess;
|
|
766
|
+
if (!semantic || typeof semantic.met !== "boolean" || !boundedText(semantic.summary || "")) {
|
|
767
|
+
return { ok: false, error: "strict verdict requires semanticSuccess.met and a non-empty summary" };
|
|
768
|
+
}
|
|
769
|
+
if (input.verdict.status === "approved" && !semantic.met) {
|
|
770
|
+
return { ok: false, error: "approved verdict requires semanticSuccess.met=true" };
|
|
771
|
+
}
|
|
772
|
+
const issues = (input.verdict.issues ?? []).map(String).map((issue) => boundedText(issue)).filter(Boolean);
|
|
773
|
+
if (input.verdict.status === "approved" && issues.length > 0) {
|
|
774
|
+
return { ok: false, error: "approved verdict cannot carry unresolved issues" };
|
|
775
|
+
}
|
|
776
|
+
if (input.verdict.status === "changes_requested" && issues.length === 0) {
|
|
777
|
+
return { ok: false, error: "changes_requested verdict requires at least one actionable issue" };
|
|
778
|
+
}
|
|
779
|
+
if (input.verdict.status === "approved" && concreteEvidence.length === 0) {
|
|
780
|
+
return { ok: false, error: "approved verdict requires typed reviewer evidence" };
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
concreteEvidence = (input.evidence ?? [])
|
|
785
|
+
.map(String)
|
|
786
|
+
.map((item) => item.trim())
|
|
787
|
+
.filter(Boolean)
|
|
788
|
+
.map((item) => ({
|
|
789
|
+
kind: "review",
|
|
790
|
+
source: `legacy:${input.reviewerId}`,
|
|
791
|
+
workerId: input.reviewerId,
|
|
792
|
+
role: "reviewer",
|
|
793
|
+
summary: boundedText(item),
|
|
794
|
+
recordedAt: timestamp,
|
|
795
|
+
}));
|
|
796
|
+
if (input.verdict.status === "approved" && concreteEvidence.length === 0) {
|
|
797
|
+
return { ok: false, error: "approved verdict requires at least one non-empty concrete evidence item" };
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
const fromStage = journal.stage;
|
|
801
|
+
const verdict = {
|
|
802
|
+
status: input.verdict.status,
|
|
803
|
+
reviewerId: input.reviewerId,
|
|
804
|
+
summary,
|
|
805
|
+
issues: (input.verdict.issues ?? []).map(String).map((issue) => boundedText(issue)).filter(Boolean),
|
|
806
|
+
semanticSuccess: input.verdict.semanticSuccess
|
|
807
|
+
? { met: input.verdict.semanticSuccess.met, summary: boundedText(input.verdict.semanticSuccess.summary) }
|
|
808
|
+
: { met: input.verdict.status === "approved", summary: "legacy reviewer verdict" },
|
|
809
|
+
recordedAt: timestamp,
|
|
810
|
+
};
|
|
811
|
+
journal.reviewerVerdict = verdict;
|
|
812
|
+
appendRunEvent(journal, {
|
|
813
|
+
type: "review_recorded",
|
|
814
|
+
at: timestamp,
|
|
815
|
+
fromStage,
|
|
816
|
+
toStage: fromStage,
|
|
817
|
+
role: "reviewer",
|
|
818
|
+
workerId: input.reviewerId,
|
|
819
|
+
summary: `${verdict.status}: ${verdict.summary}`,
|
|
820
|
+
});
|
|
821
|
+
journal.evidence.push(...concreteEvidence);
|
|
822
|
+
if (journal.workflowVersion === 2 && journal.usage)
|
|
823
|
+
journal.usage.evidenceItems += concreteEvidence.length;
|
|
824
|
+
if (verdict.status === "approved") {
|
|
825
|
+
journal.stage = "completed";
|
|
826
|
+
journal.cursor = "done";
|
|
827
|
+
journal.completedAt = timestamp;
|
|
828
|
+
appendRunEvent(journal, {
|
|
829
|
+
type: "completed",
|
|
830
|
+
at: timestamp,
|
|
831
|
+
fromStage,
|
|
832
|
+
toStage: "completed",
|
|
833
|
+
role: "reviewer",
|
|
834
|
+
workerId: input.reviewerId,
|
|
835
|
+
summary: verdict.summary,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
else if (journal.workflowVersion === 2) {
|
|
839
|
+
moveToRetryOrBlocked(journal, timestamp, verdict.summary, input.reviewerId, "reviewer");
|
|
840
|
+
}
|
|
841
|
+
else if (journal.attempt < journal.maxAttempts) {
|
|
842
|
+
journal.attempt += 1;
|
|
843
|
+
journal.stage = "retry_required";
|
|
844
|
+
delete journal.workers.reviewer;
|
|
845
|
+
appendRunEvent(journal, {
|
|
846
|
+
type: "retry_required", at: timestamp, attempt: journal.attempt, fromStage,
|
|
847
|
+
toStage: "retry_required", role: "reviewer", workerId: input.reviewerId, summary: verdict.summary,
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
journal.stage = "blocked";
|
|
852
|
+
journal.blockedAt = timestamp;
|
|
853
|
+
appendRunEvent(journal, {
|
|
854
|
+
type: "blocked", at: timestamp, fromStage, toStage: "blocked", role: "reviewer",
|
|
855
|
+
workerId: input.reviewerId, summary: verdict.summary,
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
journal.updatedAt = timestamp;
|
|
859
|
+
saveRunJournal(journal, options);
|
|
860
|
+
return { ok: true, journal };
|
|
861
|
+
}
|
|
862
|
+
/** Discover only unfinished governed runs bound to this task or orchestration session. */
|
|
863
|
+
function findBoundUnfinishedRunIds(binding, options = {}) {
|
|
864
|
+
const taskId = binding.taskId?.trim();
|
|
865
|
+
const sessionId = binding.sessionId?.trim();
|
|
866
|
+
if (!taskId && !sessionId)
|
|
867
|
+
return [];
|
|
868
|
+
let names;
|
|
869
|
+
try {
|
|
870
|
+
names = fs.readdirSync(runsDir(options));
|
|
871
|
+
}
|
|
872
|
+
catch {
|
|
873
|
+
return [];
|
|
874
|
+
}
|
|
875
|
+
const matches = [];
|
|
876
|
+
for (const name of names.sort()) {
|
|
877
|
+
if (!name.endsWith(".json"))
|
|
878
|
+
continue;
|
|
879
|
+
const runId = name.slice(0, -5);
|
|
880
|
+
if (!validRunId(runId))
|
|
881
|
+
continue;
|
|
882
|
+
const journal = loadRunJournal(runId, options);
|
|
883
|
+
if (!journal)
|
|
884
|
+
continue;
|
|
885
|
+
// A task binding is more specific than a goal-derived session. When present, use it
|
|
886
|
+
// exclusively so two distinct tasks with identical goal text cannot block one another.
|
|
887
|
+
const bound = taskId ? journal.taskId === taskId : Boolean(sessionId && journal.sessionId === sessionId);
|
|
888
|
+
if (!bound)
|
|
889
|
+
continue;
|
|
890
|
+
if (journal.stage === "completed" &&
|
|
891
|
+
journal.reviewerVerdict?.status === "approved" &&
|
|
892
|
+
(journal.workflowVersion !== 2 || journal.reviewerVerdict.semanticSuccess.met))
|
|
893
|
+
continue;
|
|
894
|
+
matches.push(runId);
|
|
895
|
+
}
|
|
896
|
+
return matches;
|
|
897
|
+
}
|
|
898
|
+
/** A governed loop may finish only after every run it touched has reviewer-approved completion. */
|
|
899
|
+
function runFinishBlocker(runIds, options = {}) {
|
|
900
|
+
for (const runId of runIds) {
|
|
901
|
+
const journal = loadRunJournal(runId, options);
|
|
902
|
+
if (!journal)
|
|
903
|
+
return `governed run ${runId} is missing or corrupt`;
|
|
904
|
+
if (journal.stage !== "completed" ||
|
|
905
|
+
journal.reviewerVerdict?.status !== "approved" ||
|
|
906
|
+
(journal.workflowVersion === 2 && !journal.reviewerVerdict.semanticSuccess.met)) {
|
|
907
|
+
return `governed run ${runId} is ${journal.stage} (attempt ${journal.attempt}/${journal.maxAttempts}); a distinct terminal reviewer must approve it through complete_run`;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return undefined;
|
|
911
|
+
}
|
|
912
|
+
//# sourceMappingURL=run-journal.js.map
|