yaaw-se 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/cli/commands/install.js +1 -1
  2. package/dist/cli/commands/install.js.map +1 -1
  3. package/dist/installer/status.js +74 -0
  4. package/dist/installer/status.js.map +1 -1
  5. package/dist/installer/verify.d.ts +2 -1
  6. package/dist/installer/verify.js +6 -2
  7. package/dist/installer/verify.js.map +1 -1
  8. package/dist/integrations/codex-runtime.d.ts +39 -0
  9. package/dist/integrations/codex-runtime.js +89 -0
  10. package/dist/integrations/codex-runtime.js.map +1 -1
  11. package/dist/integrations/codex.js +103 -16
  12. package/dist/integrations/codex.js.map +1 -1
  13. package/dist/payload/integrations/codex/yaaw-runtime.md +38 -10
  14. package/dist/payload/payload-files.json +95 -55
  15. package/dist/payload/payload.json +1 -1
  16. package/dist/payload/yaaw-core/system/core/context-loading.md +12 -8
  17. package/dist/payload/yaaw-core/system/core/dispatch-execution.md +18 -2
  18. package/dist/payload/yaaw-core/system/core/execution-context.md +31 -23
  19. package/dist/payload/yaaw-core/system/core/invalidation.md +29 -12
  20. package/dist/payload/yaaw-core/system/core/io-contract.md +8 -0
  21. package/dist/payload/yaaw-core/system/core/lifecycle.md +11 -16
  22. package/dist/payload/yaaw-core/system/core/routing.md +7 -7
  23. package/dist/payload/yaaw-core/system/core/transitions.md +2 -5
  24. package/dist/payload/yaaw-core/system/registries/handoff-policy.json +85 -0
  25. package/dist/payload/yaaw-core/system/roles/orchestrator.md +16 -14
  26. package/dist/payload/yaaw-core/system/rules/repository-identity.md +31 -17
  27. package/dist/payload/yaaw-core/system/schemas/evidence-v1.schema.json +28 -0
  28. package/dist/payload/yaaw-core/system/schemas/evidence-v2.schema.json +19 -0
  29. package/dist/payload/yaaw-core/system/schemas/evidence.schema.json +20 -31
  30. package/dist/payload/yaaw-core/system/schemas/repository-identity.schema.json +47 -0
  31. package/dist/payload/yaaw-core/system/schemas/review-v1.schema.json +19 -0
  32. package/dist/payload/yaaw-core/system/schemas/review-v2.schema.json +17 -0
  33. package/dist/payload/yaaw-core/system/schemas/review.schema.json +26 -13
  34. package/dist/payload/yaaw-core/system/templates/evidence.json +19 -2
  35. package/dist/payload/yaaw-core/system/templates/handoff.json +9 -15
  36. package/dist/payload/yaaw-core/system/templates/observed-state.json +13 -1
  37. package/dist/payload/yaaw-core/system/templates/review.md +14 -5
  38. package/dist/payload/yaaw-core/system/tools/orchestration-runtime.mjs +689 -0
  39. package/dist/payload/yaaw-core/system/tools/repository-identity.mjs +92 -0
  40. package/dist/payload/yaaw-core/system/workflows/implementation/implement-ticket.md +4 -0
  41. package/dist/payload/yaaw-core/system/workflows/implementation/repair-ticket.md +4 -0
  42. package/dist/payload/yaaw-core/system/workflows/implementation/verify-ticket.md +3 -0
  43. package/dist/payload/yaaw-core/system/workflows/orchestration/determine-next-action.md +15 -15
  44. package/dist/payload/yaaw-core/system/workflows/orchestration/dispatch.md +22 -16
  45. package/dist/payload/yaaw-core/system/workflows/orchestration/inspect-state.md +12 -15
  46. package/dist/payload/yaaw-core/system/workflows/orchestration/recover-interruption.md +4 -0
  47. package/dist/payload/yaaw-core/system/workflows/orchestration/route.md +19 -11
  48. package/dist/payload/yaaw-core/system/workflows/planning/replan.md +4 -0
  49. package/dist/payload/yaaw-core/system/workflows/review/inspect-change.md +4 -1
  50. package/dist/tui/configuration-updates.js +12 -1
  51. package/dist/tui/configuration-updates.js.map +1 -1
  52. package/package.json +1 -1
@@ -0,0 +1,689 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
4
+ import { basename, join, resolve } from "node:path";
5
+
6
+ const args = process.argv.slice(2);
7
+ let workspaceArg = ".";
8
+ let mode = "prepare";
9
+ for (let i = 0; i < args.length; i += 1) {
10
+ if (args[i] === "--workspace" && args[i + 1]) {
11
+ workspaceArg = args[i + 1];
12
+ i += 1;
13
+ } else if (args[i] === "--inspect-only") {
14
+ mode = "inspect";
15
+ } else if (args[i] === "--check-handoff") {
16
+ mode = "check";
17
+ } else {
18
+ process.stderr.write("usage: orchestration-runtime.mjs [--workspace <path>] [--inspect-only|--check-handoff]\n");
19
+ process.exit(2);
20
+ }
21
+ }
22
+
23
+ const workspace = resolve(workspaceArg);
24
+ const systemRoot = join(workspace, ".yaaw-core", "system");
25
+ const projectRoot = join(workspace, ".yaaw-core", "project");
26
+ const runtimeRoot = join(workspace, ".yaaw-core", "runtime");
27
+ const observedPath = join(runtimeRoot, "observed-state.json");
28
+ const handoffPath = join(runtimeRoot, "handoff.json");
29
+
30
+ const readJson = async path => JSON.parse(await readFile(path, "utf8"));
31
+ const readJsonOrNull = async path => {
32
+ try { return await readJson(path); } catch { return null; }
33
+ };
34
+ const exists = async path => {
35
+ try { await readFile(path); return true; } catch { return false; }
36
+ };
37
+ const unique = values => [...new Set(values.filter(Boolean))];
38
+ const normalizeStatus = value => String(value ?? "").trim().toLowerCase();
39
+
40
+ function parseScalar(raw) {
41
+ const value = raw.trim();
42
+ if (value === "null") return null;
43
+ if (value === "true") return true;
44
+ if (value === "false") return false;
45
+ if (/^[0-9]+$/.test(value)) return Number(value);
46
+ if (value.startsWith("[") || value.startsWith("{")) {
47
+ try { return JSON.parse(value); } catch {}
48
+ }
49
+ return value.replace(/^["']|["']$/g, "");
50
+ }
51
+
52
+ async function frontmatter(path) {
53
+ try {
54
+ const text = await readFile(path, "utf8");
55
+ const lines = text.split(/\r?\n/);
56
+ if (lines[0]?.trim() !== "---") return null;
57
+ const end = lines.slice(1).findIndex(line => line.trim() === "---");
58
+ if (end < 0) return null;
59
+ const data = {};
60
+ for (const line of lines.slice(1, end + 1)) {
61
+ if (!line.trim() || line.trim().startsWith("#") || !line.includes(":")) continue;
62
+ const index = line.indexOf(":");
63
+ data[line.slice(0, index).trim()] = parseScalar(line.slice(index + 1));
64
+ }
65
+ return data;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ async function files(dir, pattern) {
72
+ try {
73
+ return (await readdir(dir, { withFileTypes: true }))
74
+ .filter(entry => entry.isFile() && pattern.test(entry.name))
75
+ .map(entry => join(dir, entry.name))
76
+ .sort();
77
+ } catch {
78
+ return [];
79
+ }
80
+ }
81
+
82
+ async function reviewRecord(path) {
83
+ try {
84
+ const text = await readFile(path, "utf8");
85
+ const lines = text.split(/\r?\n/);
86
+ if (lines[0]?.trim() !== "---") return null;
87
+ const closing = lines.slice(1).findIndex(line => line.trim() === "---");
88
+ if (closing < 0) return null;
89
+ const data = { repository: {} };
90
+ let repository = false;
91
+ for (const line of lines.slice(1, closing + 1)) {
92
+ if (/^repository:\s*$/.test(line)) {
93
+ repository = true;
94
+ continue;
95
+ }
96
+ const nested = line.match(/^\s{2}([A-Za-z0-9_]+):\s*(.*)$/);
97
+ if (repository && nested) {
98
+ data.repository[nested[1]] = parseScalar(nested[2]);
99
+ continue;
100
+ }
101
+ if (!/^\S/.test(line)) continue;
102
+ repository = false;
103
+ const top = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
104
+ if (top) data[top[1]] = parseScalar(top[2]);
105
+ }
106
+ if (!data.repository.worktree_digest && data.reviewed_worktree_digest) {
107
+ data.repository.worktree_digest = data.reviewed_worktree_digest;
108
+ data.repository.head_commit = data.reviewed_head_commit ?? null;
109
+ data.repository.dirty = data.reviewed_dirty ?? null;
110
+ }
111
+ return data;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ async function evidenceRecords(paths) {
118
+ const records = [];
119
+ for (const path of paths) {
120
+ try {
121
+ const value = await readJson(path);
122
+ records.push({ path, value });
123
+ } catch {}
124
+ }
125
+ return records;
126
+ }
127
+
128
+ function runJsonTool(path, toolArgs) {
129
+ const result = spawnSync(process.execPath, [path, ...toolArgs], {
130
+ cwd: workspace,
131
+ encoding: "utf8",
132
+ windowsHide: true,
133
+ maxBuffer: 64 * 1024 * 1024
134
+ });
135
+ let parsed;
136
+ try {
137
+ parsed = JSON.parse(result.stdout || "{}");
138
+ } catch {
139
+ throw new Error((result.stderr || "").trim() || `invalid JSON from ${basename(path)}`);
140
+ }
141
+ return { parsed, status: result.status ?? 2, stderr: (result.stderr || "").trim() };
142
+ }
143
+
144
+ function frameworkView(raw, overrideStatus = null) {
145
+ const status = overrideStatus ?? raw?.status ?? "UNKNOWN";
146
+ return {
147
+ yaaw_version: String(raw?.package_version ?? "unknown"),
148
+ system_schema: Number(raw?.system_schema ?? 1),
149
+ installation_schema: Number(raw?.installation_schema ?? 1),
150
+ project_schema: Number(raw?.project_schema ?? 1),
151
+ manifest_digest: String(raw?.manifest_digest ?? "sha256:unknown"),
152
+ integrity_status: status,
153
+ modified: Array.isArray(raw?.modified) ? raw.modified : [],
154
+ missing: Array.isArray(raw?.missing) ? raw.missing : [],
155
+ local_overrides: Array.isArray(raw?.local_overrides) ? raw.local_overrides : [],
156
+ repair_required: status !== "HEALTHY"
157
+ };
158
+ }
159
+
160
+ function handoffFramework(framework) {
161
+ return {
162
+ yaaw_version: framework.yaaw_version,
163
+ system_schema: framework.system_schema,
164
+ installation_schema: framework.installation_schema,
165
+ project_schema: framework.project_schema,
166
+ manifest_digest: framework.manifest_digest,
167
+ integrity_status: "HEALTHY"
168
+ };
169
+ }
170
+
171
+ function repositoryView(raw) {
172
+ return {
173
+ status: raw?.status ?? "UNAVAILABLE",
174
+ workspace_scope: String(raw?.workspace_scope ?? "."),
175
+ git_root_relation: raw?.git_root_relation ?? "unknown",
176
+ head_commit: raw?.head_commit ?? null,
177
+ dirty: typeof raw?.dirty === "boolean" ? raw.dirty : null,
178
+ worktree_digest: raw?.worktree_digest ?? null,
179
+ error: raw?.error ?? null
180
+ };
181
+ }
182
+
183
+ function contractFailure(frameworkRaw, errors) {
184
+ return {
185
+ framework: frameworkView(frameworkRaw, "CONTRACT_INCONSISTENT"),
186
+ errors
187
+ };
188
+ }
189
+
190
+ async function loadContracts() {
191
+ const registryRoot = join(systemRoot, "registries");
192
+ const [workflows, execution, roleIo, routing, handoffPolicy, artifacts] = await Promise.all([
193
+ readJson(join(registryRoot, "workflows.json")),
194
+ readJson(join(registryRoot, "execution-policy.json")),
195
+ readJson(join(registryRoot, "role-io.json")),
196
+ readJson(join(registryRoot, "routing-policy.json")),
197
+ readJson(join(registryRoot, "handoff-policy.json")),
198
+ readJson(join(registryRoot, "artifacts.json"))
199
+ ]);
200
+ const errors = [];
201
+ for (const [workflow, policy] of Object.entries(handoffPolicy.workflows ?? {})) {
202
+ const registered = workflows[workflow];
203
+ const executionPolicy = execution.workflows?.[workflow];
204
+ if (!registered) errors.push(`handoff-policy references unknown workflow ${workflow}`);
205
+ if (!executionPolicy) errors.push(`workflow ${workflow} has no execution policy`);
206
+ const role = registered?.role;
207
+ const io = roleIo.roles?.[role];
208
+ if (!io) errors.push(`workflow ${workflow} has unknown role ${role}`);
209
+ if (io) {
210
+ const badReads = (policy.reads ?? []).filter(id => !io.reads.includes(id));
211
+ const badWrites = (policy.writes ?? []).filter(id => !io.writes.includes(id));
212
+ if (badReads.length) errors.push(`${workflow} reads exceed role I/O: ${badReads.join(",")}`);
213
+ if (badWrites.length) errors.push(`${workflow} writes exceed role I/O: ${badWrites.join(",")}`);
214
+ }
215
+ }
216
+ return { workflows, execution, roleIo, routing, handoffPolicy, artifacts, errors };
217
+ }
218
+
219
+ function idFromSpec(value) {
220
+ if (!value) return null;
221
+ const match = String(value).match(/SPEC-[0-9]+/);
222
+ return match ? match[0] : null;
223
+ }
224
+
225
+ async function inspectArtifacts(state) {
226
+ const productPath = join(projectRoot, "product.md");
227
+ const engineeringPath = join(projectRoot, "engineering.md");
228
+ const specPaths = await files(join(projectRoot, "specs"), /^SPEC-[0-9]+\.md$/);
229
+ const ticketPaths = await files(join(projectRoot, "tickets"), /^TASK-[0-9]+\.md$/);
230
+ const reviewPaths = await files(join(projectRoot, "reviews"), /^TASK-[0-9]+-R[0-9]+\.md$/);
231
+ const evidencePaths = await files(join(projectRoot, "evidence"), /\.json$/);
232
+ const researchPaths = await files(join(projectRoot, "research"), /^RSH-.*\.md$/);
233
+ const rulePaths = await files(join(projectRoot, "rules"), /\.md$/);
234
+
235
+ const product = await frontmatter(productPath);
236
+ const engineering = await frontmatter(engineeringPath);
237
+ const specs = {};
238
+ for (const path of specPaths) {
239
+ const meta = await frontmatter(path);
240
+ if (meta?.id) specs[meta.id] = { path, meta };
241
+ }
242
+ const tickets = {};
243
+ for (const path of ticketPaths) {
244
+ const meta = await frontmatter(path);
245
+ if (meta?.id) tickets[meta.id] = { path, meta };
246
+ }
247
+
248
+ const activeSpecId = idFromSpec(state?.planning?.active_spec);
249
+ const activeSpec = activeSpecId ? specs[activeSpecId] ?? null : null;
250
+ return {
251
+ product: { path: productPath, meta: product },
252
+ engineering: { path: engineeringPath, meta: engineering },
253
+ specs,
254
+ tickets,
255
+ activeSpecId,
256
+ activeSpec,
257
+ reviews: reviewPaths,
258
+ evidence: evidencePaths,
259
+ research: researchPaths,
260
+ rules: rulePaths
261
+ };
262
+ }
263
+
264
+ function artifactSummary(artifacts) {
265
+ const rel = path => path.replace(workspace + "/", "").replaceAll("\\", "/");
266
+ return {
267
+ product: artifacts.product.meta ? { path: rel(artifacts.product.path), metadata: artifacts.product.meta } : null,
268
+ engineering: artifacts.engineering.meta ? { path: rel(artifacts.engineering.path), metadata: artifacts.engineering.meta } : null,
269
+ active_spec: artifacts.activeSpec ? { path: rel(artifacts.activeSpec.path), metadata: artifacts.activeSpec.meta } : null,
270
+ specs: Object.fromEntries(Object.entries(artifacts.specs).map(([id, value]) => [id, { path: rel(value.path), metadata: value.meta }])),
271
+ tickets: Object.fromEntries(Object.entries(artifacts.tickets).map(([id, value]) => [id, { path: rel(value.path), metadata: value.meta }])),
272
+ reviews: artifacts.reviews.map(rel),
273
+ evidence: artifacts.evidence.map(rel)
274
+ };
275
+ }
276
+
277
+ function ticketSourceCurrent(state, artifacts, ticketId) {
278
+ const ticket = artifacts.tickets[ticketId]?.meta;
279
+ const spec = artifacts.activeSpec?.meta;
280
+ if (!ticket || !spec) return false;
281
+ return Number(ticket.product_revision) === Number(state.product?.revision)
282
+ && Number(ticket.engineering_revision) === Number(state.planning?.revision)
283
+ && Number(ticket.spec_revision) === Number(spec.revision)
284
+ && String(ticket.spec ?? "") === String(artifacts.activeSpecId ?? "");
285
+ }
286
+
287
+ function currentEvidenceFor(records, state, artifacts, ticketId) {
288
+ const ticketRevision = Number(artifacts.tickets[ticketId]?.meta?.revision);
289
+ const specRevision = Number(artifacts.activeSpec?.meta?.revision);
290
+ return records.filter(({ value }) =>
291
+ value?.ticket === ticketId
292
+ && value?.kind === "implementation_verification"
293
+ && Number(value?.ticket_revision) === ticketRevision
294
+ && Number(value?.spec_revision) === specRevision
295
+ );
296
+ }
297
+
298
+ async function acceptanceInconsistencies(state, artifacts, repository) {
299
+ const issues = [];
300
+ const evidence = await evidenceRecords(artifacts.evidence);
301
+ for (const ticketId of Object.keys(state?.tickets ?? {}).sort()) {
302
+ const lifecycle = state.tickets[ticketId];
303
+ if (!["PASS", "IN_PROGRESS", "READY"].includes(lifecycle)) continue;
304
+ const currentEvidence = currentEvidenceFor(evidence, state, artifacts, ticketId);
305
+ const currentVerification = repository.status === "READY"
306
+ ? currentEvidence.filter(({ value }) => value?.repository?.worktree_digest === repository.worktree_digest)
307
+ : currentEvidence;
308
+
309
+ if (lifecycle === "IN_PROGRESS" && currentVerification.length) {
310
+ issues.push(`IMPLEMENTATION_VERIFIED_NOT_REVIEWED:${ticketId}`);
311
+ continue;
312
+ }
313
+ if (lifecycle === "READY" && currentVerification.length) {
314
+ issues.push(`IMPLEMENTATION_ALREADY_PRESENT:${ticketId}`);
315
+ continue;
316
+ }
317
+ if (lifecycle !== "PASS") continue;
318
+
319
+ if (!ticketSourceCurrent(state, artifacts, ticketId)) {
320
+ issues.push(`TICKET_SOURCE_STALE:${ticketId}`);
321
+ continue;
322
+ }
323
+
324
+ const reviewPath = artifacts.reviews
325
+ .filter(path => basename(path).startsWith(ticketId + "-R"))
326
+ .sort((a, b) => {
327
+ const ar = Number(basename(a).match(/-R([0-9]+)/)?.[1] ?? 0);
328
+ const br = Number(basename(b).match(/-R([0-9]+)/)?.[1] ?? 0);
329
+ return br - ar;
330
+ })[0];
331
+ if (!reviewPath) {
332
+ issues.push(`REVIEW_MISSING:${ticketId}`);
333
+ } else {
334
+ const review = await reviewRecord(reviewPath);
335
+ const ticketRevision = Number(artifacts.tickets[ticketId]?.meta?.revision);
336
+ const specRevision = Number(artifacts.activeSpec?.meta?.revision);
337
+ if (!review || review.ticket !== ticketId || review.result !== "PASS"
338
+ || Number(review.ticket_revision) !== ticketRevision
339
+ || Number(review.spec_revision) !== specRevision) {
340
+ issues.push(`REVIEW_MISSING:${ticketId}`);
341
+ } else if (!review.repository?.worktree_digest) {
342
+ issues.push(`LEGACY_IDENTITY_UNVERIFIABLE:${ticketId}`);
343
+ } else if (repository.status === "READY" && review.repository.worktree_digest !== repository.worktree_digest) {
344
+ issues.push(`REVIEW_REPOSITORY_STALE:${ticketId}`);
345
+ }
346
+ }
347
+
348
+ if (!currentEvidence.length) {
349
+ issues.push(`VERIFICATION_MISSING:${ticketId}`);
350
+ } else if (repository.status === "READY" && !currentVerification.length) {
351
+ issues.push(`VERIFICATION_REPOSITORY_STALE:${ticketId}`);
352
+ }
353
+ }
354
+ return unique(issues);
355
+ }
356
+
357
+ function metadataInconsistencies(state, artifacts) {
358
+ const inconsistencies = [];
359
+ if (!state || state.schema !== "yaaw.project-state/v1") {
360
+ inconsistencies.push("PROJECT_STATE_MISSING_OR_INVALID");
361
+ return inconsistencies;
362
+ }
363
+ const product = artifacts.product.meta;
364
+ if (product) {
365
+ if (Number(product.revision ?? -1) !== Number(state.product?.revision ?? -2)) inconsistencies.push("PRODUCT_REVISION_MISMATCH");
366
+ if (normalizeStatus(product.status) !== normalizeStatus(state.product?.status)) inconsistencies.push("PRODUCT_STATUS_MISMATCH");
367
+ } else if ((state.product?.status ?? "missing") !== "missing") {
368
+ inconsistencies.push("PRODUCT_ARTIFACT_MISSING");
369
+ }
370
+ const engineering = artifacts.engineering.meta;
371
+ if (engineering) {
372
+ if (Number(engineering.revision ?? -1) !== Number(state.planning?.revision ?? -2)) inconsistencies.push("ENGINEERING_REVISION_MISMATCH");
373
+ if (normalizeStatus(engineering.status) !== normalizeStatus(state.planning?.status)) inconsistencies.push("ENGINEERING_STATUS_MISMATCH");
374
+ if (normalizeStatus(engineering.readiness) !== normalizeStatus(state.planning?.readiness)) inconsistencies.push("ENGINEERING_READINESS_MISMATCH");
375
+ } else if ((state.planning?.status ?? "missing") !== "missing") {
376
+ inconsistencies.push("ENGINEERING_ARTIFACT_MISSING");
377
+ }
378
+ if (state.planning?.active_spec && !artifacts.activeSpec) inconsistencies.push("ACTIVE_SPEC_MISSING");
379
+ for (const ticketId of Object.keys(state.tickets ?? {})) {
380
+ if (!artifacts.tickets[ticketId]) inconsistencies.push(`TICKET_ARTIFACT_MISSING:${ticketId}`);
381
+ }
382
+ return inconsistencies;
383
+ }
384
+
385
+ function dependenciesSatisfied(ticketId, artifacts, state) {
386
+ const deps = artifacts.tickets[ticketId]?.meta?.dependencies;
387
+ if (!Array.isArray(deps)) return true;
388
+ return deps.every(dep => state.tickets?.[dep] === "PASS");
389
+ }
390
+
391
+ function route(state, artifacts, routing) {
392
+ if (state.blocker) return { kind: "BLOCKED", terminal: "BLOCKED", reason: state.blocker.kind ?? "PROJECT_BLOCKED" };
393
+ if (normalizeStatus(state.product?.status) !== "ready") return { kind: "WORKFLOW", workflow: routing.product_unready_workflow };
394
+ const tickets = state.tickets ?? {};
395
+
396
+ for (const rule of routing.ticket_state_precedence ?? []) {
397
+ if (rule.state === "REPLAN_REQUIRED") {
398
+ const ticket = Object.keys(tickets).sort().find(id => tickets[id] === rule.state);
399
+ if (ticket) return { kind: "WORKFLOW", workflow: rule.workflow, ticket };
400
+ }
401
+ }
402
+
403
+ if (normalizeStatus(state.planning?.status) !== "ready" || state.planning?.readiness !== "PASS") {
404
+ return { kind: "WORKFLOW", workflow: routing.planning_unready_workflow };
405
+ }
406
+ if (!artifacts.activeSpec || normalizeStatus(artifacts.activeSpec.meta?.status) !== "accepted") {
407
+ return { kind: "WORKFLOW", workflow: routing.missing_spec_workflow };
408
+ }
409
+ if (Object.keys(tickets).length === 0) {
410
+ return { kind: "WORKFLOW", workflow: routing.missing_tickets_workflow };
411
+ }
412
+
413
+ for (const rule of routing.ticket_state_precedence ?? []) {
414
+ if (rule.state === "REPLAN_REQUIRED") continue;
415
+ const candidates = Object.keys(tickets).sort().filter(id => tickets[id] === rule.state);
416
+ const ticket = rule.state === "READY"
417
+ ? candidates.find(id => dependenciesSatisfied(id, artifacts, state))
418
+ : candidates[0];
419
+ if (ticket) {
420
+ if (rule.workflow.startsWith("orchestration.")) return { kind: "ROOT_ACTION", workflow: rule.workflow, ticket };
421
+ return { kind: "WORKFLOW", workflow: rule.workflow, ticket };
422
+ }
423
+ }
424
+
425
+ const states = Object.values(tickets);
426
+ if (states.some(value => value === "BLOCKED")) return { kind: "BLOCKED", terminal: "BLOCKED", reason: "TICKET_BLOCKED" };
427
+ if (states.length && states.every(value => value === "PASS" || value === "CANCELLED")) {
428
+ if (state.phase === "complete") return { kind: "TERMINAL", terminal: routing.complete_terminal };
429
+ return { kind: "WORKFLOW", workflow: routing.next_frontier_workflow };
430
+ }
431
+ return { kind: "ROOT_ACTION", workflow: "orchestration.recover-interruption", reason: "NO_SAFE_SEMANTIC_ROUTE" };
432
+ }
433
+
434
+ function requirementAllows(requirement, repository) {
435
+ if (requirement === "NONE") return true;
436
+ if (requirement === "IDENTITY") return repository.status === "READY";
437
+ return repository.status === "READY" || repository.status === "UNVERSIONED";
438
+ }
439
+
440
+ function rel(path) {
441
+ return path.replace(workspace + "/", "").replaceAll("\\", "/");
442
+ }
443
+
444
+ function referencesFor(policy, artifacts, ticketId) {
445
+ const refs = [];
446
+ for (const artifact of policy.reads ?? []) {
447
+ if (artifact === "product" && artifacts.product.meta) refs.push(rel(artifacts.product.path));
448
+ else if (artifact === "engineering" && artifacts.engineering.meta) refs.push(rel(artifacts.engineering.path));
449
+ else if (artifact === "engineering_research") refs.push(...artifacts.research.map(rel));
450
+ else if (artifact === "spec" && artifacts.activeSpec) refs.push(rel(artifacts.activeSpec.path));
451
+ else if (artifact === "ticket" && ticketId && artifacts.tickets[ticketId]) refs.push(rel(artifacts.tickets[ticketId].path));
452
+ else if (artifact === "project_rule") refs.push(...artifacts.rules.map(rel));
453
+ else if (artifact === "review" && ticketId) refs.push(...artifacts.reviews.filter(path => basename(path).startsWith(ticketId + "-")).map(rel));
454
+ else if (artifact === "evidence" && ticketId) refs.push(...artifacts.evidence.filter(path => basename(path).includes(ticketId)).map(rel));
455
+ else if (artifact === "state") refs.push(".yaaw-core/project/state.json");
456
+ }
457
+ return unique(refs).sort();
458
+ }
459
+
460
+ function revisionsFor(state, artifacts, ticketId) {
461
+ const revisions = {};
462
+ if (Number.isInteger(state.product?.revision)) revisions.product = state.product.revision;
463
+ if (Number.isInteger(state.planning?.revision)) revisions.engineering = state.planning.revision;
464
+ if (Number.isInteger(artifacts.activeSpec?.meta?.revision)) revisions.spec = artifacts.activeSpec.meta.revision;
465
+ if (ticketId && Number.isInteger(artifacts.tickets[ticketId]?.meta?.revision)) revisions.ticket = artifacts.tickets[ticketId].meta.revision;
466
+ return revisions;
467
+ }
468
+
469
+ async function expertiseFor(policy, workflowRole, artifacts, ticketId) {
470
+ const registry = await readJson(join(systemRoot, "registries", "expertise.json"));
471
+ const requested = [...(policy.required_expertise ?? [])];
472
+ if (policy.ticket_expertise && ticketId) {
473
+ const ticketExpertise = artifacts.tickets[ticketId]?.meta?.expertise;
474
+ if (Array.isArray(ticketExpertise)) requested.push(...ticketExpertise);
475
+ }
476
+ return unique(requested)
477
+ .filter(id => registry[id] && (registry[id].usable_by ?? []).includes(workflowRole))
478
+ .sort();
479
+ }
480
+
481
+ function activeArtifact(policy, artifacts, ticketId) {
482
+ if (policy.active_artifact === "product") return artifacts.product.meta ? rel(artifacts.product.path) : null;
483
+ if (policy.active_artifact === "engineering") return artifacts.engineering.meta ? rel(artifacts.engineering.path) : null;
484
+ if (policy.active_artifact === "spec") return artifacts.activeSpec ? rel(artifacts.activeSpec.path) : null;
485
+ if (policy.active_artifact === "ticket") return ticketId && artifacts.tickets[ticketId] ? rel(artifacts.tickets[ticketId].path) : null;
486
+ return null;
487
+ }
488
+
489
+ async function buildHandoff(selected, contracts, state, artifacts, framework, repository) {
490
+ const registered = contracts.workflows[selected.workflow];
491
+ const policy = contracts.handoffPolicy.workflows[selected.workflow];
492
+ const execution = contracts.execution.workflows[selected.workflow];
493
+ if (!registered || !policy || !execution) {
494
+ throw new Error(`missing deterministic handoff contract for ${selected.workflow}`);
495
+ }
496
+ const role = registered.role;
497
+ const roleContract = contracts.roleIo.roles[role];
498
+ return {
499
+ schema: "yaaw.handoff/v2",
500
+ framework: handoffFramework(framework),
501
+ role,
502
+ workflow: selected.workflow,
503
+ desired_intent: policy.desired_intent ?? null,
504
+ active_artifact: activeArtifact(policy, artifacts, selected.ticket),
505
+ references: referencesFor(policy, artifacts, selected.ticket),
506
+ reads: unique(policy.reads ?? []),
507
+ writes: unique(policy.writes ?? []),
508
+ forbidden_writes: unique(roleContract.forbidden_writes ?? []),
509
+ revisions: revisionsFor(state, artifacts, selected.ticket),
510
+ expertise: await expertiseFor(policy, role, artifacts, selected.ticket),
511
+ host_expertise: [],
512
+ expected_output: policy.expected_output,
513
+ result_vocabulary: unique(policy.result_vocabulary ?? []),
514
+ repository_requirement: execution.repository_requirement,
515
+ repository,
516
+ transition_sequence: state.transition_sequence ?? 0
517
+ };
518
+ }
519
+
520
+ function deepEqual(a, b) {
521
+ return JSON.stringify(a) === JSON.stringify(b);
522
+ }
523
+
524
+ async function clearHandoff() {
525
+ await rm(handoffPath, { force: true });
526
+ }
527
+
528
+ async function main() {
529
+ const frameworkTool = join(systemRoot, "tools", "framework-integrity.mjs");
530
+ const identityTool = join(systemRoot, "tools", "repository-identity.mjs");
531
+
532
+ let frameworkRun;
533
+ try {
534
+ frameworkRun = runJsonTool(frameworkTool, ["--workspace", workspace]);
535
+ } catch (error) {
536
+ frameworkRun = { parsed: { status: "UNKNOWN", package_version: null, manifest_digest: null }, status: 2, stderr: String(error?.message ?? error) };
537
+ }
538
+ const frameworkRaw = frameworkRun.parsed;
539
+ let framework = frameworkView(frameworkRaw);
540
+
541
+ let contracts;
542
+ try {
543
+ contracts = await loadContracts();
544
+ } catch (error) {
545
+ const observed = {
546
+ schema: "yaaw.observed-state/v2",
547
+ framework: frameworkView(frameworkRaw, "CONTRACT_INCONSISTENT"),
548
+ repository: repositoryView(null),
549
+ transition_sequence: 0,
550
+ claims: {},
551
+ artifacts: {},
552
+ inconsistencies: [`FRAMEWORK_CONTRACT_INCONSISTENCY:${String(error?.message ?? error)}`],
553
+ candidate_actions: []
554
+ };
555
+ if (mode !== "check") {
556
+ await mkdir(runtimeRoot, { recursive: true });
557
+ await writeFile(observedPath, JSON.stringify(observed, null, 2) + "\n");
558
+ await clearHandoff();
559
+ }
560
+ return { schema: "yaaw.orchestration-runtime/v1", status: "FRAMEWORK_STOP", reason: "FRAMEWORK_CONTRACT_INCONSISTENCY", observed };
561
+ }
562
+
563
+ if (contracts.errors.length) {
564
+ framework = contractFailure(frameworkRaw, contracts.errors).framework;
565
+ }
566
+
567
+ let repositoryRaw = null;
568
+ try {
569
+ repositoryRaw = runJsonTool(identityTool, ["--workspace", workspace]).parsed;
570
+ } catch (error) {
571
+ repositoryRaw = { status: "UNAVAILABLE", workspace_scope: ".", git_root_relation: "unknown", head_commit: null, dirty: null, worktree_digest: null, error: String(error?.message ?? error) };
572
+ }
573
+ const repository = repositoryView(repositoryRaw);
574
+
575
+ const state = await readJsonOrNull(join(projectRoot, "state.json"));
576
+ const artifacts = await inspectArtifacts(state);
577
+ const inconsistencies = metadataInconsistencies(state, artifacts);
578
+ inconsistencies.push(...await acceptanceInconsistencies(state, artifacts, repository));
579
+ if (contracts.errors.length) inconsistencies.unshift(...contracts.errors.map(error => `FRAMEWORK_CONTRACT_INCONSISTENCY:${error}`));
580
+
581
+ const observed = {
582
+ schema: "yaaw.observed-state/v2",
583
+ framework,
584
+ repository,
585
+ transition_sequence: state?.transition_sequence ?? 0,
586
+ claims: state ?? {},
587
+ artifacts: artifactSummary(artifacts),
588
+ inconsistencies,
589
+ candidate_actions: []
590
+ };
591
+
592
+ if (framework.integrity_status !== "HEALTHY") {
593
+ observed.candidate_actions = ["INSTALLER_REPAIR"];
594
+ if (mode !== "check") {
595
+ await mkdir(runtimeRoot, { recursive: true });
596
+ await writeFile(observedPath, JSON.stringify(observed, null, 2) + "\n");
597
+ await clearHandoff();
598
+ }
599
+ const reason = framework.integrity_status === "CONTRACT_INCONSISTENT"
600
+ ? "FRAMEWORK_CONTRACT_INCONSISTENCY"
601
+ : ["UNKNOWN", "MANIFEST_INVALID"].includes(framework.integrity_status)
602
+ ? "FRAMEWORK_INTEGRITY_UNKNOWN"
603
+ : "FRAMEWORK_INTEGRITY_VIOLATION";
604
+ return { schema: "yaaw.orchestration-runtime/v1", status: "FRAMEWORK_STOP", reason, observed };
605
+ }
606
+
607
+ if (inconsistencies.length) {
608
+ observed.candidate_actions = ["orchestration.reconcile-state"];
609
+ if (mode !== "check") {
610
+ await mkdir(runtimeRoot, { recursive: true });
611
+ await writeFile(observedPath, JSON.stringify(observed, null, 2) + "\n");
612
+ await clearHandoff();
613
+ }
614
+ return { schema: "yaaw.orchestration-runtime/v1", status: "RECONCILE_REQUIRED", workflow: "orchestration.reconcile-state", observed };
615
+ }
616
+
617
+ if (mode === "inspect") {
618
+ await mkdir(runtimeRoot, { recursive: true });
619
+ await writeFile(observedPath, JSON.stringify(observed, null, 2) + "\n");
620
+ return { schema: "yaaw.orchestration-runtime/v1", status: "INSPECTED", observed };
621
+ }
622
+
623
+ const selected = route(state, artifacts, contracts.routing);
624
+ observed.candidate_actions = [selected.workflow ?? selected.terminal ?? selected.reason ?? selected.kind];
625
+
626
+ if (selected.kind === "ROOT_ACTION") {
627
+ if (mode !== "check") {
628
+ await mkdir(runtimeRoot, { recursive: true });
629
+ await writeFile(observedPath, JSON.stringify(observed, null, 2) + "\n");
630
+ await clearHandoff();
631
+ }
632
+ return { schema: "yaaw.orchestration-runtime/v1", status: "ROOT_ACTION", workflow: selected.workflow, ticket: selected.ticket ?? null, observed };
633
+ }
634
+
635
+ if (selected.kind === "TERMINAL" || selected.kind === "BLOCKED") {
636
+ if (mode !== "check") {
637
+ await mkdir(runtimeRoot, { recursive: true });
638
+ await writeFile(observedPath, JSON.stringify(observed, null, 2) + "\n");
639
+ await clearHandoff();
640
+ }
641
+ return { schema: "yaaw.orchestration-runtime/v1", status: selected.kind, terminal: selected.terminal, reason: selected.reason ?? null, observed };
642
+ }
643
+
644
+ const execution = contracts.execution.workflows[selected.workflow];
645
+ if (!requirementAllows(execution.repository_requirement, repository)) {
646
+ if (mode !== "check") {
647
+ await mkdir(runtimeRoot, { recursive: true });
648
+ await writeFile(observedPath, JSON.stringify(observed, null, 2) + "\n");
649
+ await clearHandoff();
650
+ }
651
+ return {
652
+ schema: "yaaw.orchestration-runtime/v1",
653
+ status: "BLOCKED",
654
+ terminal: "BLOCKED",
655
+ reason: execution.repository_requirement === "IDENTITY" ? "REPOSITORY_IDENTITY_UNAVAILABLE" : "REPOSITORY_INSPECTION_UNAVAILABLE",
656
+ workflow: selected.workflow,
657
+ observed
658
+ };
659
+ }
660
+
661
+ const handoff = await buildHandoff(selected, contracts, state, artifacts, framework, repository);
662
+
663
+ if (mode === "check") {
664
+ const persisted = await readJsonOrNull(handoffPath);
665
+ if (persisted && deepEqual(persisted, handoff)) {
666
+ return { schema: "yaaw.orchestration-runtime/v1", status: "HANDOFF_FRESH", workflow: handoff.workflow, role: handoff.role };
667
+ }
668
+ return { schema: "yaaw.orchestration-runtime/v1", status: "HANDOFF_STALE", workflow: selected.workflow, reason: "HANDOFF_BASIS_CHANGED" };
669
+ }
670
+
671
+ await mkdir(runtimeRoot, { recursive: true });
672
+ await writeFile(observedPath, JSON.stringify(observed, null, 2) + "\n");
673
+ await writeFile(handoffPath, JSON.stringify(handoff, null, 2) + "\n");
674
+ return { schema: "yaaw.orchestration-runtime/v1", status: "DISPATCH_READY", workflow: handoff.workflow, role: handoff.role, handoff };
675
+ }
676
+
677
+ try {
678
+ const result = await main();
679
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
680
+ process.exitCode = ["DISPATCH_READY", "INSPECTED", "HANDOFF_FRESH", "ROOT_ACTION", "TERMINAL"].includes(result.status) ? 0 : 2;
681
+ } catch (error) {
682
+ process.stdout.write(JSON.stringify({
683
+ schema: "yaaw.orchestration-runtime/v1",
684
+ status: "FRAMEWORK_STOP",
685
+ reason: "FRAMEWORK_CONTRACT_INCONSISTENCY",
686
+ error: String(error?.message ?? error)
687
+ }, null, 2) + "\n");
688
+ process.exitCode = 2;
689
+ }