@kontextmind/kxm 0.6.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/.claude-plugin/marketplace.json +19 -0
- package/.kxm/README.md +14 -0
- package/.kxm/assets/README.md +5 -0
- package/.kxm/assets/retrospectives/README.md +5 -0
- package/.kxm/config/README.md +5 -0
- package/.kxm/config/agents.json +43 -0
- package/.kxm/config/env.example +56 -0
- package/.kxm/config/update.example.yaml +9 -0
- package/.kxm/config/workflows/fix.json +160 -0
- package/.kxm/config/workflows/jira-development.json +116 -0
- package/.kxm/config/workflows/provenance-quorum.json +150 -0
- package/.kxm/config/workflows/v04-dogfood.json +72 -0
- package/CHANGELOG.md +465 -0
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/SECURITY.md +72 -0
- package/docs/README.md +48 -0
- package/docs/agent-communication-envelopes-and-gates.md +553 -0
- package/docs/architecture.md +242 -0
- package/docs/assignment-runner.md +241 -0
- package/docs/configuration.md +361 -0
- package/docs/continuous-improvement.md +114 -0
- package/docs/getting-started.md +253 -0
- package/docs/kxm-handbook.md +1090 -0
- package/docs/operations.md +205 -0
- package/docs/provenance-gates.md +291 -0
- package/docs/skills.md +45 -0
- package/docs/templates/README.md +95 -0
- package/docs/templates/adr.md +88 -0
- package/docs/templates/architecture.md +120 -0
- package/docs/templates/bug-fix.md +109 -0
- package/docs/templates/feature.md +108 -0
- package/docs/templates/handoff.md +72 -0
- package/docs/templates/postmortem.md +77 -0
- package/docs/templates/research.md +100 -0
- package/docs/templates/review.md +85 -0
- package/docs/templates/runbook.md +73 -0
- package/docs/templates/test-plan.md +87 -0
- package/docs/templates/test-report.md +72 -0
- package/docs/test-matrix.md +121 -0
- package/docs/troubleshooting.md +249 -0
- package/docs/vnext/README.md +62 -0
- package/docs/vnext/architecture.md +185 -0
- package/docs/vnext/effects-and-recovery.md +172 -0
- package/docs/vnext/lifecycles.md +235 -0
- package/docs/vnext/migration.md +220 -0
- package/docs/vnext/routing.md +184 -0
- package/docs/vnext/synchronization.md +172 -0
- package/docs/vnext/terminology.md +240 -0
- package/docs/vnext/validation.md +335 -0
- package/docs/webhook-workflows.md +240 -0
- package/docs/workflow-guide.md +1150 -0
- package/examples/README.md +102 -0
- package/examples/provenance-workflow.json +40 -0
- package/examples/requester.ts +30 -0
- package/examples/reviewer-agent.ts +29 -0
- package/examples/roundtrip.ts +46 -0
- package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
- package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
- package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
- package/examples/vnext/.kxm/agents/planner.yaml +13 -0
- package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
- package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
- package/examples/vnext/.kxm/gates.yaml +8 -0
- package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
- package/examples/vnext/.kxm/models/implementation.yaml +14 -0
- package/examples/vnext/.kxm/models/primary.yaml +17 -0
- package/examples/vnext/.kxm/prices.yaml +111 -0
- package/examples/vnext/.kxm/project/env.yaml +7 -0
- package/examples/vnext/.kxm/project.yaml +32 -0
- package/examples/vnext/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/.kxm/workflows/default.yaml +92 -0
- package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
- package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
- package/examples/vnext/README.md +53 -0
- package/examples/vnext/records/assignment-result-recorded.json +63 -0
- package/examples/vnext/records/assignment-result.json +46 -0
- package/examples/vnext/records/context-candidate.json +42 -0
- package/examples/vnext/records/delivery-manifest.json +66 -0
- package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
- package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
- package/examples/vnext/records/run-created.json +54 -0
- package/examples/vnext/records/sync-event.json +65 -0
- package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
- package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
- package/examples/workflow-signal.ts +63 -0
- package/package.json +129 -0
- package/plugins/kxm/.claude-plugin/plugin.json +73 -0
- package/plugins/kxm/.mcp.json +19 -0
- package/plugins/kxm/README.md +93 -0
- package/plugins/kxm/dist/cli.js +42853 -0
- package/plugins/kxm/dist/client.js +416 -0
- package/plugins/kxm/dist/core.js +1823 -0
- package/plugins/kxm/dist/extension.js +3797 -0
- package/plugins/kxm/dist/mcp-server.js +17104 -0
- package/plugins/kxm/dist/runtime.js +23361 -0
- package/plugins/kxm/dist/server.js +13640 -0
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
- package/plugins/kxm/package.json +12 -0
- package/plugins/kxm/skills/kxm/SKILL.md +97 -0
- package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
- package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
- package/plugins/kxm/src/arbiter.ts +355 -0
- package/plugins/kxm/src/artifacts-exist.ts +62 -0
- package/plugins/kxm/src/autocomplete.ts +236 -0
- package/plugins/kxm/src/cli.ts +3707 -0
- package/plugins/kxm/src/client.ts +614 -0
- package/plugins/kxm/src/commands.ts +1063 -0
- package/plugins/kxm/src/config.ts +290 -0
- package/plugins/kxm/src/context/providers.ts +101 -0
- package/plugins/kxm/src/context-packet.ts +332 -0
- package/plugins/kxm/src/context.ts +499 -0
- package/plugins/kxm/src/core.ts +6 -0
- package/plugins/kxm/src/database.ts +563 -0
- package/plugins/kxm/src/diagnostics.ts +184 -0
- package/plugins/kxm/src/envelope.ts +118 -0
- package/plugins/kxm/src/extension.ts +895 -0
- package/plugins/kxm/src/external-effects.ts +299 -0
- package/plugins/kxm/src/github-watch.ts +255 -0
- package/plugins/kxm/src/hub-binding.ts +160 -0
- package/plugins/kxm/src/hub.ts +2502 -0
- package/plugins/kxm/src/improve.ts +383 -0
- package/plugins/kxm/src/inbox.ts +10 -0
- package/plugins/kxm/src/kxm-install-kind.ts +113 -0
- package/plugins/kxm/src/kxm-update-config.ts +39 -0
- package/plugins/kxm/src/kxm-update.ts +238 -0
- package/plugins/kxm/src/local-snapshot.ts +406 -0
- package/plugins/kxm/src/logger.ts +198 -0
- package/plugins/kxm/src/mcp-server.ts +143 -0
- package/plugins/kxm/src/memory.ts +385 -0
- package/plugins/kxm/src/nous-pi.ts +287 -0
- package/plugins/kxm/src/nous-provider.ts +729 -0
- package/plugins/kxm/src/price-calc.ts +87 -0
- package/plugins/kxm/src/prices.ts +121 -0
- package/plugins/kxm/src/protocol.ts +172 -0
- package/plugins/kxm/src/recovery.ts +211 -0
- package/plugins/kxm/src/redact.ts +26 -0
- package/plugins/kxm/src/retrospective.ts +400 -0
- package/plugins/kxm/src/routing.ts +830 -0
- package/plugins/kxm/src/runtime.ts +9 -0
- package/plugins/kxm/src/server.ts +117 -0
- package/plugins/kxm/src/session-work.ts +571 -0
- package/plugins/kxm/src/session.ts +184 -0
- package/plugins/kxm/src/skills.ts +535 -0
- package/plugins/kxm/src/state.ts +326 -0
- package/plugins/kxm/src/store.ts +637 -0
- package/plugins/kxm/src/studio-layout.ts +268 -0
- package/plugins/kxm/src/suggest.ts +162 -0
- package/plugins/kxm/src/task-manager.ts +244 -0
- package/plugins/kxm/src/telemetry.ts +116 -0
- package/plugins/kxm/src/tui.ts +1046 -0
- package/plugins/kxm/src/vnext-bindings.ts +403 -0
- package/plugins/kxm/src/vnext-config.ts +1646 -0
- package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
- package/plugins/kxm/src/vnext-engine-command.ts +533 -0
- package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
- package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
- package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
- package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
- package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
- package/plugins/kxm/src/vnext-engine.ts +2458 -0
- package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
- package/plugins/kxm/src/vnext-harness.ts +1142 -0
- package/plugins/kxm/src/vnext-init.ts +430 -0
- package/plugins/kxm/src/vnext-migrate.ts +1848 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
- package/plugins/kxm/src/vnext-permission.ts +936 -0
- package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
- package/plugins/kxm/src/vnext-repair.ts +1094 -0
- package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
- package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
- package/plugins/kxm/src/vnext-runtime.ts +663 -0
- package/plugins/kxm/src/vnext-template.ts +247 -0
- package/plugins/kxm/src/wiki.ts +313 -0
- package/plugins/kxm/src/workflow.ts +1548 -0
- package/schemas/vnext/README.md +46 -0
- package/schemas/vnext/agent.schema.json +40 -0
- package/schemas/vnext/assignment-result.schema.json +66 -0
- package/schemas/vnext/backup-manifest.schema.json +89 -0
- package/schemas/vnext/candidate.schema.json +109 -0
- package/schemas/vnext/common.schema.json +422 -0
- package/schemas/vnext/context-candidate.schema.json +76 -0
- package/schemas/vnext/context-packet.schema.json +192 -0
- package/schemas/vnext/delivery-manifest.schema.json +159 -0
- package/schemas/vnext/environment.schema.json +66 -0
- package/schemas/vnext/gate-registry.schema.json +109 -0
- package/schemas/vnext/handoff-manifest.schema.json +146 -0
- package/schemas/vnext/init-operation.schema.json +61 -0
- package/schemas/vnext/local-repository-bindings.schema.json +30 -0
- package/schemas/vnext/memory-record.schema.json +45 -0
- package/schemas/vnext/migration-decision.schema.json +26 -0
- package/schemas/vnext/migration-plan.schema.json +123 -0
- package/schemas/vnext/migration-receipt.schema.json +52 -0
- package/schemas/vnext/model.schema.json +42 -0
- package/schemas/vnext/permission-diff.schema.json +57 -0
- package/schemas/vnext/prices.schema.json +115 -0
- package/schemas/vnext/project.schema.json +85 -0
- package/schemas/vnext/repository.schema.json +24 -0
- package/schemas/vnext/run-event.schema.json +460 -0
- package/schemas/vnext/session-brief.schema.json +153 -0
- package/schemas/vnext/sync-event.schema.json +234 -0
- package/schemas/vnext/template-provenance.schema.json +38 -0
- package/schemas/vnext/workflow.schema.json +248 -0
- package/scripts/assignment-run.d.mts +354 -0
- package/scripts/assignment-run.mjs +4451 -0
- package/scripts/build-runtime.mjs +56 -0
- package/scripts/check-generated.mjs +77 -0
- package/scripts/check-versions.mjs +34 -0
- package/scripts/emit-codex-artifacts.d.mts +9 -0
- package/scripts/emit-codex-artifacts.mjs +91 -0
- package/scripts/harness-run.d.mts +83 -0
- package/scripts/harness-run.mjs +2095 -0
- package/scripts/kxm-hub.mjs +105 -0
- package/scripts/kxm-publish-npm.mjs +327 -0
- package/scripts/kxm-release-github.mjs +472 -0
- package/scripts/kxm-runtime-supervisor.mjs +7 -0
- package/scripts/kxm-worker.mjs +1127 -0
- package/scripts/kxm.mjs +27 -0
- package/scripts/roster-policy.d.mts +20 -0
- package/scripts/roster-policy.mjs +161 -0
- package/scripts/smoke-multi-pi.mjs +479 -0
|
@@ -0,0 +1,4451 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Dev assignment helper. M3a is exported pure validation: no spawn, identity
|
|
3
|
+
// consumption, dispatch, or .git writes. M3b adds role templates, exclusive
|
|
4
|
+
// identity/output consume, one-shot v2 harness dispatch, immutable completion,
|
|
5
|
+
// and one pending telemetry append. M4a adds fixed verify/validate-ci
|
|
6
|
+
// witness execution, stored-manifest identity/plan/recording binding,
|
|
7
|
+
// and candidate-bound private receipts. M4b W1 adds exact-commit developer
|
|
8
|
+
// acceptance from the latest passed verify receipt and designated critics.
|
|
9
|
+
// Not a product assignment layer.
|
|
10
|
+
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
|
+
import {
|
|
14
|
+
chmodSync,
|
|
15
|
+
existsSync,
|
|
16
|
+
lstatSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
readdirSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
renameSync,
|
|
21
|
+
rmdirSync,
|
|
22
|
+
rmSync,
|
|
23
|
+
realpathSync,
|
|
24
|
+
statSync,
|
|
25
|
+
writeFileSync,
|
|
26
|
+
} from "node:fs";
|
|
27
|
+
import nodePath from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import {
|
|
30
|
+
BEHAVIORAL_HASH_VERSION,
|
|
31
|
+
ROUTING_RECORD_SCHEMA,
|
|
32
|
+
agentWorker,
|
|
33
|
+
behavioralConfigHash,
|
|
34
|
+
nowIso,
|
|
35
|
+
parseRoutingRecord,
|
|
36
|
+
workerResult,
|
|
37
|
+
} from "../plugins/kxm/dist/core.js";
|
|
38
|
+
import {
|
|
39
|
+
CONTEXT_OCCUPANCY_UNKNOWN,
|
|
40
|
+
COST_BASIS,
|
|
41
|
+
EFFORT,
|
|
42
|
+
ERROR_CODES,
|
|
43
|
+
MODEL_CLAIM_STATUSES,
|
|
44
|
+
REQUEST_SCHEMA,
|
|
45
|
+
RESULT_SCHEMA,
|
|
46
|
+
REVIEW_VERDICTS,
|
|
47
|
+
TOKEN_BASIS,
|
|
48
|
+
TRANSPORT_STAGES,
|
|
49
|
+
TRANSPORT_STATUSES,
|
|
50
|
+
CLAIM_SOURCES,
|
|
51
|
+
piProviderOf,
|
|
52
|
+
preflightRequest,
|
|
53
|
+
runHarness,
|
|
54
|
+
} from "./harness-run.mjs";
|
|
55
|
+
import {
|
|
56
|
+
canonicalVendor,
|
|
57
|
+
loadTrustedRosterPolicy,
|
|
58
|
+
resolveBoundPolicy,
|
|
59
|
+
} from "./roster-policy.mjs";
|
|
60
|
+
|
|
61
|
+
const resolvePath = nodePath.resolve;
|
|
62
|
+
const { join, basename, dirname, relative } = nodePath;
|
|
63
|
+
|
|
64
|
+
export const ASSIGNMENT_SCHEMA = "kxm.assignment.v1";
|
|
65
|
+
export const PLAN_POINTER_SCHEMA = "kxm.plan-pointer.v1";
|
|
66
|
+
export const PLAN_POINTER_FILENAME = "plan-current.json";
|
|
67
|
+
export const COMPLETION_SCHEMA = "kxm.assignment-completion.v1";
|
|
68
|
+
export const REFUSAL_SCHEMA = "kxm.assignment-refusal.v1";
|
|
69
|
+
export const RUNNER_CODES = Object.freeze([
|
|
70
|
+
"manifest_invalid",
|
|
71
|
+
"route_invalid",
|
|
72
|
+
"base_invalid",
|
|
73
|
+
"plan_ref_invalid",
|
|
74
|
+
"inputs_invalid",
|
|
75
|
+
"contract_invalid",
|
|
76
|
+
"rework_invalid",
|
|
77
|
+
"output_dir_unsafe",
|
|
78
|
+
"output_dir_exists",
|
|
79
|
+
"identity_taken",
|
|
80
|
+
"output_dir_taken",
|
|
81
|
+
"mkdir_failed",
|
|
82
|
+
"prompt_render_failed",
|
|
83
|
+
"record_write_failed",
|
|
84
|
+
"snapshot_failed",
|
|
85
|
+
"sidecar_failed",
|
|
86
|
+
"routing_record_failed",
|
|
87
|
+
"telemetry_failed",
|
|
88
|
+
"completion_missing",
|
|
89
|
+
"completion_invalid",
|
|
90
|
+
"witness_binding_invalid",
|
|
91
|
+
"plan_mismatch",
|
|
92
|
+
"recording_unresolved",
|
|
93
|
+
"dirty_baseline",
|
|
94
|
+
"gate_failed",
|
|
95
|
+
"candidate_changed",
|
|
96
|
+
"commit_missing",
|
|
97
|
+
"commit_tree_mismatch",
|
|
98
|
+
"witness_missing",
|
|
99
|
+
"witness_stale",
|
|
100
|
+
"witness_failed",
|
|
101
|
+
"critic_invalid",
|
|
102
|
+
"critic_tree_mismatch",
|
|
103
|
+
"critic_block",
|
|
104
|
+
"accepted_exists",
|
|
105
|
+
"record_outside_task",
|
|
106
|
+
"observation_invalid",
|
|
107
|
+
"source_invalid",
|
|
108
|
+
"source_already_recorded",
|
|
109
|
+
"attribution_invalid",
|
|
110
|
+
"history_conflict",
|
|
111
|
+
"unknown",
|
|
112
|
+
]);
|
|
113
|
+
export const VERIFY_WITNESS_ID = "verify";
|
|
114
|
+
export const VALIDATE_CI_WITNESS_ID = "validate-ci";
|
|
115
|
+
export const WITNESS_IDS = Object.freeze(["verify", "validate-ci"]);
|
|
116
|
+
export const FIXED_GATES = Object.freeze({
|
|
117
|
+
verify: Object.freeze(["npm", "run", "verify"]),
|
|
118
|
+
"validate-ci": Object.freeze(["npm", "run", "validate:ci"]),
|
|
119
|
+
});
|
|
120
|
+
export const WITNESS_SCHEMA = "kxm.assignment-witness.v1";
|
|
121
|
+
export const WITNESS_LATEST_SCHEMA = "kxm.assignment-witness-latest.v1";
|
|
122
|
+
export const WITNESS_RESULTS = Object.freeze(["passed", "failed", "refused"]);
|
|
123
|
+
export const WITNESS_CODES = Object.freeze([
|
|
124
|
+
"completion_missing",
|
|
125
|
+
"completion_invalid",
|
|
126
|
+
"witness_binding_invalid",
|
|
127
|
+
"plan_mismatch",
|
|
128
|
+
"recording_unresolved",
|
|
129
|
+
"dirty_baseline",
|
|
130
|
+
"snapshot_failed",
|
|
131
|
+
"gate_failed",
|
|
132
|
+
"candidate_changed",
|
|
133
|
+
"record_write_failed",
|
|
134
|
+
"unknown",
|
|
135
|
+
]);
|
|
136
|
+
export const ACCEPTED_SCHEMA = "kxm.task-accepted.v1";
|
|
137
|
+
export const ACCEPTED_FILENAME = "accepted.json";
|
|
138
|
+
export const COST_OBSERVATION_SCHEMA = "kxm.cost-observation.v1";
|
|
139
|
+
export const ATTRIBUTION_SCHEMA = "kxm.assignment-attribution.v1";
|
|
140
|
+
export const ATTRIBUTION_CLASSES = Object.freeze(["orchestration", "model", "environment", "unclassified"]);
|
|
141
|
+
export const ACCEPTED_NOTE = "developer record; not human approval, hub peer evidence, CI success, or merged/released status";
|
|
142
|
+
export const REQUIRED_ACCEPT_CRITICS = Object.freeze({
|
|
143
|
+
"review-arch": Object.freeze({ harness: "claude", model: "fable", role: "reviewer-arch" }),
|
|
144
|
+
"review-cli": Object.freeze({ harness: "codex", model: "gpt-5.6-sol", role: "reviewer-cli" }),
|
|
145
|
+
});
|
|
146
|
+
export const ASSIGNMENT_KINDS = Object.freeze([
|
|
147
|
+
"plan",
|
|
148
|
+
"implement",
|
|
149
|
+
"repair",
|
|
150
|
+
"review-arch",
|
|
151
|
+
"review-cli",
|
|
152
|
+
]);
|
|
153
|
+
export const KIND_ROLES = Object.freeze({
|
|
154
|
+
plan: "planner",
|
|
155
|
+
implement: "writer",
|
|
156
|
+
repair: "writer",
|
|
157
|
+
"review-arch": "reviewer-arch",
|
|
158
|
+
"review-cli": "reviewer-cli",
|
|
159
|
+
});
|
|
160
|
+
export const REVIEW_KINDS = Object.freeze(["review-arch", "review-cli"]);
|
|
161
|
+
export const WRITER_KINDS = Object.freeze(["implement", "repair"]);
|
|
162
|
+
export const CLEAN_BASE_KINDS = Object.freeze(["plan", "implement", "repair"]);
|
|
163
|
+
export const ASSIGNMENT_DISPATCH_SCHEMA = "kxm.assignment-dispatch.v1";
|
|
164
|
+
export const TELEMETRY_SCHEMA = "kxm.telemetry.v1";
|
|
165
|
+
export const RECORDING_RESOLUTION_SCHEMA = "kxm.assignment-recording-resolution.v1";
|
|
166
|
+
export const READ_MECHANISMS = Object.freeze({
|
|
167
|
+
claude: "Use only the Read, Glob, and Grep tools. Do not use the shell.",
|
|
168
|
+
codex: "You run in a read-only sandbox. Read-only shell inspection with cat, sed, rg, and git is allowed. Do not write files, update the index, or commit.",
|
|
169
|
+
grok: "Use repository tools. Do not use subagents or web search. The candidate cwd is an instruction, not an OS sandbox claim.",
|
|
170
|
+
pi: "Use only the read, grep, find, and ls tools. This helper is not a long-lived native worker.",
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const ASSIGNMENT_KEYS = Object.freeze([
|
|
174
|
+
"schema",
|
|
175
|
+
"task_id",
|
|
176
|
+
"assignment_id",
|
|
177
|
+
"rework_of",
|
|
178
|
+
"kind",
|
|
179
|
+
"harness",
|
|
180
|
+
"model",
|
|
181
|
+
"effort",
|
|
182
|
+
"permission",
|
|
183
|
+
"cwd",
|
|
184
|
+
"task_dir",
|
|
185
|
+
"base",
|
|
186
|
+
"plan_ref",
|
|
187
|
+
"inputs",
|
|
188
|
+
"contract",
|
|
189
|
+
"output_dir",
|
|
190
|
+
"timeout_ms",
|
|
191
|
+
"max_turns",
|
|
192
|
+
]);
|
|
193
|
+
const OPTIONAL_ASSIGNMENT_KEYS = Object.freeze(["rework_of", "timeout_ms", "max_turns"]);
|
|
194
|
+
const BASE_KINDS = Object.freeze(["clean", "staged"]);
|
|
195
|
+
const PLAN_REF_KINDS = Object.freeze(["current", "bootstrap"]);
|
|
196
|
+
const POINTER_KEYS = Object.freeze([
|
|
197
|
+
"schema",
|
|
198
|
+
"task_id",
|
|
199
|
+
"generation",
|
|
200
|
+
"plan_path",
|
|
201
|
+
"plan_sha256",
|
|
202
|
+
"base_commit",
|
|
203
|
+
"settled_decisions",
|
|
204
|
+
"updated_at",
|
|
205
|
+
"supersedes",
|
|
206
|
+
]);
|
|
207
|
+
const SUPERSEDES_KEYS = Object.freeze([
|
|
208
|
+
"generation",
|
|
209
|
+
"plan_path",
|
|
210
|
+
"plan_sha256",
|
|
211
|
+
"base_commit",
|
|
212
|
+
"updated_at",
|
|
213
|
+
]);
|
|
214
|
+
const COMPLETION_LINK_KEYS = Object.freeze(["schema", "assignment_id", "task_id"]);
|
|
215
|
+
const HEX40 = /^[a-f0-9]{40}$/;
|
|
216
|
+
const HEX64 = /^[a-f0-9]{64}$/;
|
|
217
|
+
const IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
218
|
+
const STAGED_INDEX_STATES = new Set(["M", "A", "D", "T"]);
|
|
219
|
+
const UNMERGED_XY = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
|
|
220
|
+
|
|
221
|
+
function failClosed(message, code = "unknown") {
|
|
222
|
+
const error = new Error(message);
|
|
223
|
+
error.failClosed = true;
|
|
224
|
+
error.runnerCode = RUNNER_CODES.includes(code) ? code : "unknown";
|
|
225
|
+
return error;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function lstatOrNull(path, lstatImpl) {
|
|
229
|
+
try {
|
|
230
|
+
return lstatImpl(path);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (error && error.code === "ENOENT") return null;
|
|
233
|
+
throw failClosed(`cannot lstat path: ${error?.code ?? "unknown"}`, "unknown");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function isPlainObject(value) {
|
|
238
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function closedObject(value, keys, label, optional = [], code = "manifest_invalid") {
|
|
242
|
+
if (!isPlainObject(value)) {
|
|
243
|
+
throw failClosed(`${label} must be an object`, code);
|
|
244
|
+
}
|
|
245
|
+
const unknown = Object.keys(value).filter((key) => !keys.includes(key));
|
|
246
|
+
if (unknown.length > 0) {
|
|
247
|
+
throw failClosed(`unknown ${label} field(s) ${unknown.join(", ")}`, code);
|
|
248
|
+
}
|
|
249
|
+
for (const key of keys) {
|
|
250
|
+
if (value[key] === undefined && !optional.includes(key)) {
|
|
251
|
+
throw failClosed(`${label}.${key} is required`, code);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function requireNonemptyString(value, label, code = "manifest_invalid") {
|
|
258
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
259
|
+
throw failClosed(`${label} is required as a nonempty string`, code);
|
|
260
|
+
}
|
|
261
|
+
return value;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function requireIdentity(value, label, code = "manifest_invalid") {
|
|
265
|
+
const id = requireNonemptyString(value, label, code);
|
|
266
|
+
if (!IDENTITY.test(id)) {
|
|
267
|
+
throw failClosed(`${label} must be a stable identity token`, code);
|
|
268
|
+
}
|
|
269
|
+
return id;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function requireHex(value, label, pattern, code = "manifest_invalid") {
|
|
273
|
+
const hex = requireNonemptyString(value, label, code);
|
|
274
|
+
if (!pattern.test(hex)) {
|
|
275
|
+
throw failClosed(`${label} must be a lowercase hex digest`, code);
|
|
276
|
+
}
|
|
277
|
+
return hex;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function requireStringArray(value, label, code = "manifest_invalid") {
|
|
281
|
+
if (!Array.isArray(value)) {
|
|
282
|
+
throw failClosed(`${label} must be an array of strings`, code);
|
|
283
|
+
}
|
|
284
|
+
if (value.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
285
|
+
throw failClosed(`${label} entries must be nonempty strings`, code);
|
|
286
|
+
}
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function sha256Bytes(bytes) {
|
|
291
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function gitArgv(cwd, args) {
|
|
295
|
+
return ["--no-optional-locks", "-C", cwd, ...args];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function runGit(cwd, args, spawnSyncImpl, { trim = true } = {}) {
|
|
299
|
+
const argv = gitArgv(cwd, args);
|
|
300
|
+
const result = spawnSyncImpl("git", argv, {
|
|
301
|
+
encoding: "utf8",
|
|
302
|
+
shell: false,
|
|
303
|
+
windowsHide: true,
|
|
304
|
+
});
|
|
305
|
+
if (result.error) {
|
|
306
|
+
throw failClosed(`git ${args[0]} failed: ${result.error.message}`, "base_invalid");
|
|
307
|
+
}
|
|
308
|
+
const stdout = String(result.stdout ?? "");
|
|
309
|
+
return { result, stdout: trim ? stdout.trim() : stdout, argv };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function git(cwd, args, spawnSyncImpl) {
|
|
313
|
+
const { result, stdout } = runGit(cwd, args, spawnSyncImpl);
|
|
314
|
+
if (result.status !== 0) {
|
|
315
|
+
const detail = String(result.stderr ?? result.stdout ?? "").trim() || `exit ${result.status}`;
|
|
316
|
+
throw failClosed(`git ${args[0]} failed: ${detail}`, "base_invalid");
|
|
317
|
+
}
|
|
318
|
+
return stdout;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function gitStatusZ(cwd, spawnSyncImpl) {
|
|
322
|
+
const { result, stdout } = runGit(
|
|
323
|
+
cwd,
|
|
324
|
+
["status", "--porcelain=v1", "-z", "--untracked-files=all", "--no-renames"],
|
|
325
|
+
spawnSyncImpl,
|
|
326
|
+
{ trim: false },
|
|
327
|
+
);
|
|
328
|
+
if (result.status !== 0) {
|
|
329
|
+
const detail = String(result.stderr ?? result.stdout ?? "").trim() || `exit ${result.status}`;
|
|
330
|
+
throw failClosed(`git status failed: ${detail}`, "base_invalid");
|
|
331
|
+
}
|
|
332
|
+
return parsePorcelainZ(stdout);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function parsePorcelainZ(stdout) {
|
|
336
|
+
if (stdout.length === 0) return [];
|
|
337
|
+
const chunks = stdout.split("\0");
|
|
338
|
+
if (chunks[chunks.length - 1] === "") chunks.pop();
|
|
339
|
+
const records = [];
|
|
340
|
+
for (const record of chunks) {
|
|
341
|
+
if (record.length < 3 || record[2] !== " ") {
|
|
342
|
+
throw failClosed("git status produced a malformed NUL record", "base_invalid");
|
|
343
|
+
}
|
|
344
|
+
records.push({ x: record[0], y: record[1], path: record.slice(3) });
|
|
345
|
+
}
|
|
346
|
+
return records;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function gitDiffIndex(cwd, tree, spawnSyncImpl) {
|
|
350
|
+
const { result } = runGit(cwd, ["diff-index", "--cached", "--quiet", tree], spawnSyncImpl);
|
|
351
|
+
if (result.error) {
|
|
352
|
+
throw failClosed(`git diff-index failed: ${result.error.message}`, "base_invalid");
|
|
353
|
+
}
|
|
354
|
+
if (result.status === 0) return "equal";
|
|
355
|
+
if (result.status === 1) return "differ";
|
|
356
|
+
const detail = String(result.stderr ?? result.stdout ?? "").trim() || `exit ${result.status}`;
|
|
357
|
+
throw failClosed(`git diff-index failed: ${detail}`, "base_invalid");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function samePath(left, right, realpathImpl) {
|
|
361
|
+
const resolveExisting = (target) => {
|
|
362
|
+
try {
|
|
363
|
+
return realpathImpl(target);
|
|
364
|
+
} catch {
|
|
365
|
+
return resolvePath(target);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
return resolveExisting(left) === resolveExisting(right);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function deepestExistingAncestor(target, lstatImpl) {
|
|
372
|
+
let current = resolvePath(target);
|
|
373
|
+
for (;;) {
|
|
374
|
+
if (lstatOrNull(current, lstatImpl)) return current;
|
|
375
|
+
const parent = dirname(current);
|
|
376
|
+
if (parent === current) return current;
|
|
377
|
+
current = parent;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function realpathOrUnknown(path, realpathImpl) {
|
|
382
|
+
try {
|
|
383
|
+
return realpathImpl(path);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
throw failClosed(`cannot realpath existing ancestor: ${error?.code ?? "unknown"}`, "unknown");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function comparablePath(target, realpathImpl, lstatImpl) {
|
|
390
|
+
const resolved = resolvePath(target);
|
|
391
|
+
let ancestor = deepestExistingAncestor(resolved, lstatImpl);
|
|
392
|
+
let realAncestor;
|
|
393
|
+
try {
|
|
394
|
+
realAncestor = realpathImpl(ancestor);
|
|
395
|
+
} catch (error) {
|
|
396
|
+
// A dangling final entry is known to exist via lstat. Compare through the
|
|
397
|
+
// parent so existence can still be output_dir_exists. Parent lstat/realpath
|
|
398
|
+
// uncertainty stays unknown.
|
|
399
|
+
if (ancestor !== resolved) {
|
|
400
|
+
throw failClosed(`cannot realpath existing ancestor: ${error?.code ?? "unknown"}`, "unknown");
|
|
401
|
+
}
|
|
402
|
+
const parent = dirname(resolved);
|
|
403
|
+
if (parent === resolved) {
|
|
404
|
+
throw failClosed(`cannot realpath existing ancestor: ${error?.code ?? "unknown"}`, "unknown");
|
|
405
|
+
}
|
|
406
|
+
ancestor = deepestExistingAncestor(parent, lstatImpl);
|
|
407
|
+
realAncestor = realpathOrUnknown(ancestor, realpathImpl);
|
|
408
|
+
}
|
|
409
|
+
if (resolved === ancestor) return realAncestor;
|
|
410
|
+
return resolvePath(realAncestor, relative(ancestor, resolved));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function isStrictAncestor(ancestor, path) {
|
|
414
|
+
const rel = relative(ancestor, path);
|
|
415
|
+
if (rel === "" || nodePath.isAbsolute(rel)) return false;
|
|
416
|
+
const parts = rel.split(nodePath.sep);
|
|
417
|
+
return parts.length > 0 && parts.every((part) => part !== "..");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function isSameOrDescendant(root, path) {
|
|
421
|
+
return path === root || isStrictAncestor(root, path);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function comparableOutputBinding(outputDir, taskDir, recordDir, cwd, lstatImpl, realpathImpl) {
|
|
425
|
+
const out = comparablePath(outputDir, realpathImpl, lstatImpl);
|
|
426
|
+
const task = comparablePath(taskDir, realpathImpl, lstatImpl);
|
|
427
|
+
const record = comparablePath(recordDir, realpathImpl, lstatImpl);
|
|
428
|
+
const gitDir = comparablePath(join(cwd, ".git"), realpathImpl, lstatImpl);
|
|
429
|
+
if (out === task || isStrictAncestor(out, task)) {
|
|
430
|
+
throw failClosed(`output_dir is unsafe: protected namespace ${outputDir}`, "output_dir_unsafe");
|
|
431
|
+
}
|
|
432
|
+
if (isStrictAncestor(task, out) && out !== record && !isStrictAncestor(record, out)) {
|
|
433
|
+
throw failClosed(`output_dir is unsafe: protected namespace ${outputDir}`, "output_dir_unsafe");
|
|
434
|
+
}
|
|
435
|
+
if (isSameOrDescendant(gitDir, out)) {
|
|
436
|
+
throw failClosed(`output_dir is unsafe: protected namespace ${outputDir}`, "output_dir_unsafe");
|
|
437
|
+
}
|
|
438
|
+
return { out, record };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function validateOutputDir(outputDir, taskDir, recordDir, cwd, lstatImpl, realpathImpl) {
|
|
442
|
+
const compared = comparableOutputBinding(outputDir, taskDir, recordDir, cwd, lstatImpl, realpathImpl);
|
|
443
|
+
if (lstatOrNull(outputDir, lstatImpl)) {
|
|
444
|
+
throw failClosed(`output_dir already exists: ${outputDir}`, "output_dir_exists");
|
|
445
|
+
}
|
|
446
|
+
return compared;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function readRegularFile(path, readImpl, existsImpl, statImpl, code = "manifest_invalid") {
|
|
450
|
+
if (!existsImpl(path)) {
|
|
451
|
+
throw failClosed(`missing path ${path}`, code);
|
|
452
|
+
}
|
|
453
|
+
let stat;
|
|
454
|
+
try {
|
|
455
|
+
stat = statImpl(path);
|
|
456
|
+
} catch {
|
|
457
|
+
throw failClosed(`unreadable path ${path}`, code);
|
|
458
|
+
}
|
|
459
|
+
if (!stat.isFile()) {
|
|
460
|
+
throw failClosed(`${path} is not a regular file`, code);
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
return readImpl(path);
|
|
464
|
+
} catch {
|
|
465
|
+
throw failClosed(`unreadable path ${path}`, code);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function recordXy(record) {
|
|
470
|
+
return `${record.x}${record.y}`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function isUnmergedRecord(record) {
|
|
474
|
+
return UNMERGED_XY.has(recordXy(record)) || record.x === "U" || record.y === "U";
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function inspectWorktree(cwd, spawnSyncImpl, realpathImpl) {
|
|
478
|
+
const inside = git(cwd, ["rev-parse", "--is-inside-work-tree"], spawnSyncImpl);
|
|
479
|
+
if (inside !== "true") {
|
|
480
|
+
throw failClosed(`cwd is not a git worktree: ${cwd}`, "base_invalid");
|
|
481
|
+
}
|
|
482
|
+
const toplevel = git(cwd, ["rev-parse", "--show-toplevel"], spawnSyncImpl);
|
|
483
|
+
if (!samePath(toplevel, cwd, realpathImpl)) {
|
|
484
|
+
throw failClosed("cwd must be the git worktree root", "base_invalid");
|
|
485
|
+
}
|
|
486
|
+
const head = git(cwd, ["rev-parse", "HEAD"], spawnSyncImpl);
|
|
487
|
+
const headTree = git(cwd, ["rev-parse", "HEAD^{tree}"], spawnSyncImpl);
|
|
488
|
+
const statusRecords = gitStatusZ(cwd, spawnSyncImpl);
|
|
489
|
+
const clean = statusRecords.every((record) => record.y === " ");
|
|
490
|
+
return { head, headTree, statusRecords, clean };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function refuseDirtyStatus(records) {
|
|
494
|
+
if (records.some(isUnmergedRecord)) {
|
|
495
|
+
throw failClosed("index has unmerged paths", "base_invalid");
|
|
496
|
+
}
|
|
497
|
+
throw failClosed("worktree has unstaged tracked edits or unignored untracked files", "base_invalid");
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function parseJsonFile(path, bytes, label, code = "manifest_invalid") {
|
|
501
|
+
try {
|
|
502
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
503
|
+
} catch {
|
|
504
|
+
throw failClosed(`${label} is malformed JSON: ${path}`, code);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function validatePointer(pointer) {
|
|
509
|
+
closedObject(pointer, POINTER_KEYS, "plan pointer", [], "plan_ref_invalid");
|
|
510
|
+
if (pointer.schema !== PLAN_POINTER_SCHEMA) {
|
|
511
|
+
throw failClosed(`plan pointer schema must be ${PLAN_POINTER_SCHEMA}, got ${pointer.schema ?? "none"}`, "plan_ref_invalid");
|
|
512
|
+
}
|
|
513
|
+
requireIdentity(pointer.task_id, "plan pointer.task_id", "plan_ref_invalid");
|
|
514
|
+
if (!Number.isInteger(pointer.generation) || pointer.generation < 1) {
|
|
515
|
+
throw failClosed("plan pointer.generation must be a positive integer", "plan_ref_invalid");
|
|
516
|
+
}
|
|
517
|
+
requireNonemptyString(pointer.plan_path, "plan pointer.plan_path", "plan_ref_invalid");
|
|
518
|
+
requireHex(pointer.plan_sha256, "plan pointer.plan_sha256", HEX64, "plan_ref_invalid");
|
|
519
|
+
requireHex(pointer.base_commit, "plan pointer.base_commit", HEX40, "plan_ref_invalid");
|
|
520
|
+
requireStringArray(pointer.settled_decisions, "plan pointer.settled_decisions", "plan_ref_invalid");
|
|
521
|
+
requireNonemptyString(pointer.updated_at, "plan pointer.updated_at", "plan_ref_invalid");
|
|
522
|
+
if (!Array.isArray(pointer.supersedes)) {
|
|
523
|
+
throw failClosed("plan pointer.supersedes must be an array", "plan_ref_invalid");
|
|
524
|
+
}
|
|
525
|
+
for (const [index, entry] of pointer.supersedes.entries()) {
|
|
526
|
+
closedObject(entry, SUPERSEDES_KEYS, `plan pointer.supersedes[${index}]`, [], "plan_ref_invalid");
|
|
527
|
+
if (!Number.isInteger(entry.generation) || entry.generation < 1) {
|
|
528
|
+
throw failClosed(`plan pointer.supersedes[${index}].generation must be a positive integer`, "plan_ref_invalid");
|
|
529
|
+
}
|
|
530
|
+
requireNonemptyString(entry.plan_path, `plan pointer.supersedes[${index}].plan_path`, "plan_ref_invalid");
|
|
531
|
+
requireHex(entry.plan_sha256, `plan pointer.supersedes[${index}].plan_sha256`, HEX64, "plan_ref_invalid");
|
|
532
|
+
requireHex(entry.base_commit, `plan pointer.supersedes[${index}].base_commit`, HEX40, "plan_ref_invalid");
|
|
533
|
+
requireNonemptyString(entry.updated_at, `plan pointer.supersedes[${index}].updated_at`, "plan_ref_invalid");
|
|
534
|
+
}
|
|
535
|
+
return pointer;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function pointerPathFor(taskDir) {
|
|
539
|
+
return join(taskDir, PLAN_POINTER_FILENAME);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function assignmentRecordDir(taskDir, assignmentId) {
|
|
543
|
+
return join(taskDir, assignmentId);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function validateTaskDir(taskDirValue, taskId, existsImpl, statImpl, realpathImpl) {
|
|
547
|
+
const taskDir = requireNonemptyString(taskDirValue, "task_dir");
|
|
548
|
+
if (!nodePath.isAbsolute(taskDir)) {
|
|
549
|
+
throw failClosed("task_dir must be an absolute path", "manifest_invalid");
|
|
550
|
+
}
|
|
551
|
+
if (!existsImpl(taskDir)) {
|
|
552
|
+
throw failClosed(`task_dir does not exist: ${taskDir}`, "manifest_invalid");
|
|
553
|
+
}
|
|
554
|
+
let stat;
|
|
555
|
+
try {
|
|
556
|
+
stat = statImpl(taskDir);
|
|
557
|
+
} catch {
|
|
558
|
+
throw failClosed(`task_dir is not a directory: ${taskDir}`, "manifest_invalid");
|
|
559
|
+
}
|
|
560
|
+
if (!stat.isDirectory()) {
|
|
561
|
+
throw failClosed(`task_dir is not a directory: ${taskDir}`, "manifest_invalid");
|
|
562
|
+
}
|
|
563
|
+
if (basename(taskDir) !== taskId) {
|
|
564
|
+
throw failClosed("task_dir final segment must equal task_id", "manifest_invalid");
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
return realpathImpl(taskDir);
|
|
568
|
+
} catch {
|
|
569
|
+
throw failClosed(`task_dir is not a directory: ${taskDir}`, "manifest_invalid");
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function validateCurrentPlanRef(planRef, taskDir, taskId, deps) {
|
|
574
|
+
closedObject(planRef, ["kind", "path", "sha256"], "plan_ref", [], "plan_ref_invalid");
|
|
575
|
+
const planPath = resolvePath(taskDir, requireNonemptyString(planRef.path, "plan_ref.path", "plan_ref_invalid"));
|
|
576
|
+
const claimed = requireHex(planRef.sha256, "plan_ref.sha256", HEX64, "plan_ref_invalid");
|
|
577
|
+
const pointerPath = pointerPathFor(taskDir);
|
|
578
|
+
const pointerBytes = readRegularFile(pointerPath, deps.readFileSync, deps.existsSync, deps.statSync, "plan_ref_invalid");
|
|
579
|
+
const pointer = validatePointer(parseJsonFile(pointerPath, pointerBytes, "plan pointer", "plan_ref_invalid"));
|
|
580
|
+
if (pointer.task_id !== taskId) {
|
|
581
|
+
throw failClosed("current plan pointer task_id does not match assignment task_id", "plan_ref_invalid");
|
|
582
|
+
}
|
|
583
|
+
const pointerPlanPath = resolvePath(taskDir, pointer.plan_path);
|
|
584
|
+
if (!samePath(pointerPlanPath, planPath, deps.realpathSync)) {
|
|
585
|
+
throw failClosed("current plan_ref.path does not match plan-current.json plan_path", "plan_ref_invalid");
|
|
586
|
+
}
|
|
587
|
+
if (pointer.plan_sha256 !== claimed) {
|
|
588
|
+
throw failClosed("plan_ref.sha256 does not match the current plan pointer", "plan_ref_invalid");
|
|
589
|
+
}
|
|
590
|
+
const planBytes = readRegularFile(planPath, deps.readFileSync, deps.existsSync, deps.statSync, "plan_ref_invalid");
|
|
591
|
+
const actual = sha256Bytes(planBytes);
|
|
592
|
+
if (actual !== claimed) {
|
|
593
|
+
throw failClosed("plan file content does not match plan_ref.sha256", "plan_ref_invalid");
|
|
594
|
+
}
|
|
595
|
+
return Object.freeze({
|
|
596
|
+
kind: "current",
|
|
597
|
+
path: planPath,
|
|
598
|
+
sha256: claimed,
|
|
599
|
+
pointer_path: pointerPath,
|
|
600
|
+
generation: pointer.generation,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function validateBootstrapPlanRef(planRef, taskDir, kind, deps) {
|
|
605
|
+
closedObject(planRef, ["kind", "reason"], "plan_ref", [], "plan_ref_invalid");
|
|
606
|
+
if (!["plan", "review-arch", "review-cli"].includes(kind)) {
|
|
607
|
+
throw failClosed("bootstrap plan_ref is only allowed for plan or review when no pointer exists", "plan_ref_invalid");
|
|
608
|
+
}
|
|
609
|
+
requireNonemptyString(planRef.reason, "plan_ref.reason", "plan_ref_invalid");
|
|
610
|
+
const pointerPath = pointerPathFor(taskDir);
|
|
611
|
+
try {
|
|
612
|
+
deps.lstatSync(pointerPath);
|
|
613
|
+
} catch (error) {
|
|
614
|
+
if (error && error.code === "ENOENT") {
|
|
615
|
+
return Object.freeze({
|
|
616
|
+
kind: "bootstrap",
|
|
617
|
+
reason: planRef.reason,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
const code = error && error.code ? error.code : "unknown";
|
|
621
|
+
throw failClosed(`bootstrap plan_ref cannot inspect ${PLAN_POINTER_FILENAME}: ${code}`, "plan_ref_invalid");
|
|
622
|
+
}
|
|
623
|
+
throw failClosed("bootstrap plan_ref is invalid because a current plan pointer already exists", "plan_ref_invalid");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function validateBase(base, kind, gitState, spawnSyncImpl, cwd) {
|
|
627
|
+
closedObject(base, ["kind", "commit", "index_tree"], "base", ["index_tree"], "base_invalid");
|
|
628
|
+
if (!BASE_KINDS.includes(base.kind)) {
|
|
629
|
+
throw failClosed(`unknown base.kind ${base.kind}`, "base_invalid");
|
|
630
|
+
}
|
|
631
|
+
const commit = requireHex(base.commit, "base.commit", HEX40, "base_invalid");
|
|
632
|
+
if (gitState.head !== commit) {
|
|
633
|
+
throw failClosed("base.commit does not match HEAD", "base_invalid");
|
|
634
|
+
}
|
|
635
|
+
if (base.kind === "clean") {
|
|
636
|
+
if (base.index_tree !== undefined) {
|
|
637
|
+
throw failClosed("clean base does not accept index_tree", "base_invalid");
|
|
638
|
+
}
|
|
639
|
+
if (gitState.statusRecords.length > 0) {
|
|
640
|
+
refuseDirtyStatus(gitState.statusRecords);
|
|
641
|
+
}
|
|
642
|
+
if (gitDiffIndex(cwd, gitState.headTree, spawnSyncImpl) !== "equal") {
|
|
643
|
+
throw failClosed("clean baseline requires the index tree to equal HEAD", "base_invalid");
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
base: Object.freeze({ kind: "clean", commit }),
|
|
647
|
+
indexTree: gitState.headTree,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
if (!REVIEW_KINDS.includes(kind)) {
|
|
651
|
+
throw failClosed("only reviews may target a staged index", "base_invalid");
|
|
652
|
+
}
|
|
653
|
+
const indexTree = requireHex(base.index_tree, "base.index_tree", HEX40, "base_invalid");
|
|
654
|
+
for (const record of gitState.statusRecords) {
|
|
655
|
+
if (isUnmergedRecord(record) || record.y !== " " || !STAGED_INDEX_STATES.has(record.x)) {
|
|
656
|
+
refuseDirtyStatus(gitState.statusRecords);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const objectType = git(cwd, ["cat-file", "-t", indexTree], spawnSyncImpl);
|
|
660
|
+
if (objectType !== "tree") {
|
|
661
|
+
throw failClosed(`base.index_tree must name a tree object, got ${objectType}`, "base_invalid");
|
|
662
|
+
}
|
|
663
|
+
if (gitDiffIndex(cwd, indexTree, spawnSyncImpl) !== "equal") {
|
|
664
|
+
throw failClosed("base.index_tree does not match the current index", "base_invalid");
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
base: Object.freeze({ kind: "staged", commit, index_tree: indexTree }),
|
|
668
|
+
indexTree,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function validateContract(contract, kind) {
|
|
673
|
+
closedObject(contract, ["boundary", "deliverables", "witness", "deferred"], "contract", [], "contract_invalid");
|
|
674
|
+
const boundary = requireNonemptyString(contract.boundary, "contract.boundary", "contract_invalid");
|
|
675
|
+
const deliverables = requireStringArray(contract.deliverables, "contract.deliverables", "contract_invalid");
|
|
676
|
+
const deferred = requireStringArray(contract.deferred, "contract.deferred", "contract_invalid");
|
|
677
|
+
closedObject(contract.witness, ["id"], "contract.witness", [], "contract_invalid");
|
|
678
|
+
const witnessId = requireNonemptyString(contract.witness.id, "contract.witness.id", "contract_invalid");
|
|
679
|
+
if (!WITNESS_IDS.includes(witnessId)) {
|
|
680
|
+
throw failClosed(`unknown contract.witness.id ${witnessId}; expected ${WITNESS_IDS.join(" or ")}`, "contract_invalid");
|
|
681
|
+
}
|
|
682
|
+
if (WRITER_KINDS.includes(kind)) {
|
|
683
|
+
if (deliverables.length < 1) {
|
|
684
|
+
throw failClosed("implement and repair require at least one deliverable", "contract_invalid");
|
|
685
|
+
}
|
|
686
|
+
if (witnessId !== VERIFY_WITNESS_ID) {
|
|
687
|
+
throw failClosed("implement and repair require the fixed verify witness", "contract_invalid");
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return Object.freeze({
|
|
691
|
+
boundary,
|
|
692
|
+
deliverables: Object.freeze([...deliverables]),
|
|
693
|
+
witness: Object.freeze({ id: witnessId }),
|
|
694
|
+
deferred: Object.freeze([...deferred]),
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function validateInputs(inputs, cwd, deps) {
|
|
699
|
+
if (!Array.isArray(inputs)) {
|
|
700
|
+
throw failClosed("inputs must be an array", "inputs_invalid");
|
|
701
|
+
}
|
|
702
|
+
const resolved = [];
|
|
703
|
+
for (const [index, item] of inputs.entries()) {
|
|
704
|
+
closedObject(item, ["path", "sha256"], `inputs[${index}]`, [], "inputs_invalid");
|
|
705
|
+
const path = resolvePath(cwd, requireNonemptyString(item.path, `inputs[${index}].path`, "inputs_invalid"));
|
|
706
|
+
const claimed = requireHex(item.sha256, `inputs[${index}].sha256`, HEX64, "inputs_invalid");
|
|
707
|
+
const bytes = readRegularFile(path, deps.readFileSync, deps.existsSync, deps.statSync, "inputs_invalid");
|
|
708
|
+
const actual = sha256Bytes(bytes);
|
|
709
|
+
if (actual !== claimed) {
|
|
710
|
+
throw failClosed(`inputs[${index}] content does not match sha256`, "inputs_invalid");
|
|
711
|
+
}
|
|
712
|
+
resolved.push(Object.freeze({ path, sha256: claimed }));
|
|
713
|
+
}
|
|
714
|
+
return Object.freeze(resolved);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function validateRework(reworkOf, taskDir, taskId, assignmentId, deps) {
|
|
718
|
+
if (reworkOf === undefined) return undefined;
|
|
719
|
+
const priorId = requireIdentity(reworkOf, "rework_of", "rework_invalid");
|
|
720
|
+
if (priorId === assignmentId) {
|
|
721
|
+
throw failClosed("rework_of cannot reference the same assignment_id", "rework_invalid");
|
|
722
|
+
}
|
|
723
|
+
const completionPath = join(assignmentRecordDir(taskDir, priorId), "completion.json");
|
|
724
|
+
if (!deps.existsSync(completionPath)) {
|
|
725
|
+
throw failClosed(`rework_of ${priorId} does not point at an existing completion`, "rework_invalid");
|
|
726
|
+
}
|
|
727
|
+
const bytes = readRegularFile(completionPath, deps.readFileSync, deps.existsSync, deps.statSync, "rework_invalid");
|
|
728
|
+
const completion = parseJsonFile(completionPath, bytes, "rework completion", "rework_invalid");
|
|
729
|
+
if (!isPlainObject(completion)) {
|
|
730
|
+
throw failClosed("rework completion is malformed", "rework_invalid");
|
|
731
|
+
}
|
|
732
|
+
for (const key of COMPLETION_LINK_KEYS) {
|
|
733
|
+
if (completion[key] === undefined) {
|
|
734
|
+
throw failClosed(`rework completion missing ${key}`, "rework_invalid");
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (completion.schema !== COMPLETION_SCHEMA) {
|
|
738
|
+
throw failClosed(`rework completion schema must be ${COMPLETION_SCHEMA}`, "rework_invalid");
|
|
739
|
+
}
|
|
740
|
+
if (completion.assignment_id !== priorId) {
|
|
741
|
+
throw failClosed("rework completion assignment_id does not match rework_of", "rework_invalid");
|
|
742
|
+
}
|
|
743
|
+
if (completion.task_id !== taskId) {
|
|
744
|
+
throw failClosed("rework completion task_id does not match assignment task_id", "rework_invalid");
|
|
745
|
+
}
|
|
746
|
+
return priorId;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
let cachedDiskPolicy = null;
|
|
750
|
+
export function getRosterPolicy(deps = {}) {
|
|
751
|
+
if (deps.rosterPolicy) {
|
|
752
|
+
return deps.rosterPolicy.policy ?? deps.rosterPolicy;
|
|
753
|
+
}
|
|
754
|
+
if (typeof deps.loadTrustedRosterPolicy === "function") {
|
|
755
|
+
const loaded = deps.loadTrustedRosterPolicy();
|
|
756
|
+
return loaded?.policy ?? loaded;
|
|
757
|
+
}
|
|
758
|
+
try {
|
|
759
|
+
return loadTrustedRosterPolicy().policy;
|
|
760
|
+
} catch (error) {
|
|
761
|
+
// When executing in a local development checkout where git status has dirty
|
|
762
|
+
// uncommitted edits or during unit tests running against in-flight code,
|
|
763
|
+
// fallback to parsing the local .kxm/roster.json.
|
|
764
|
+
if (!cachedDiskPolicy) {
|
|
765
|
+
try {
|
|
766
|
+
const diskPath = join(fileURLToPath(new URL(".", import.meta.url)), "..", ".kxm/roster.json");
|
|
767
|
+
cachedDiskPolicy = Object.freeze(JSON.parse(readFileSync(diskPath, "utf8")));
|
|
768
|
+
} catch {
|
|
769
|
+
throw failClosed(error?.message ?? "roster policy unavailable", "route_invalid");
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return cachedDiskPolicy;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function findLineupRoute(policy, role, harness, model) {
|
|
777
|
+
const lineup = policy?.lineup?.[role];
|
|
778
|
+
if (!Array.isArray(lineup)) return null;
|
|
779
|
+
for (const routeId of lineup) {
|
|
780
|
+
const route = policy.routes?.[routeId];
|
|
781
|
+
if (route && route.status === "admitted" && route.harness === harness && route.model === model) {
|
|
782
|
+
return { routeId, route };
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function assertAdmittedLineupRoute(manifest, role, policy) {
|
|
789
|
+
if (!policy || typeof policy !== "object" || !policy.routes || !policy.lineup) {
|
|
790
|
+
throw failClosed("roster policy is invalid or unparsed", "route_invalid");
|
|
791
|
+
}
|
|
792
|
+
const lineup = policy.lineup[role];
|
|
793
|
+
if (!Array.isArray(lineup) || lineup.length === 0) {
|
|
794
|
+
throw failClosed(`no admitted lineup for role ${role}`, "route_invalid");
|
|
795
|
+
}
|
|
796
|
+
const matched = findLineupRoute(policy, role, manifest.harness, manifest.model);
|
|
797
|
+
if (!matched) {
|
|
798
|
+
throw failClosed(
|
|
799
|
+
`route ${manifest.harness}/${manifest.model} is not admitted in lineup for ${role}`,
|
|
800
|
+
"route_invalid",
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
const { routeId, route } = matched;
|
|
804
|
+
if (!Array.isArray(route.roles) || !route.roles.includes(role)) {
|
|
805
|
+
throw failClosed(`route ${routeId} does not admit role ${role}`, "route_invalid");
|
|
806
|
+
}
|
|
807
|
+
if (!Array.isArray(route.permissions) || !route.permissions.includes(manifest.permission)) {
|
|
808
|
+
throw failClosed(
|
|
809
|
+
`route ${routeId} does not permit ${manifest.permission}; allowed: ${Array.isArray(route.permissions) ? route.permissions.join(", ") : "none"}`,
|
|
810
|
+
"route_invalid",
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
return matched;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function validateRoute(manifest, deps = {}) {
|
|
817
|
+
const role = KIND_ROLES[manifest.kind];
|
|
818
|
+
if (!role) {
|
|
819
|
+
throw failClosed(`unknown kind ${manifest.kind}`, "route_invalid");
|
|
820
|
+
}
|
|
821
|
+
const effort = requireNonemptyString(manifest.effort, "effort", "route_invalid");
|
|
822
|
+
if (!EFFORT.includes(effort)) {
|
|
823
|
+
throw failClosed(`unknown effort ${effort}; expected one of ${EFFORT.join(", ")}`, "route_invalid");
|
|
824
|
+
}
|
|
825
|
+
const request = {
|
|
826
|
+
schema: REQUEST_SCHEMA,
|
|
827
|
+
harness: manifest.harness,
|
|
828
|
+
role,
|
|
829
|
+
model: manifest.model,
|
|
830
|
+
permission: manifest.permission,
|
|
831
|
+
effort,
|
|
832
|
+
prompt_file: ASSIGNMENT_SCHEMA,
|
|
833
|
+
};
|
|
834
|
+
if (manifest.timeout_ms !== undefined) request.timeout_ms = manifest.timeout_ms;
|
|
835
|
+
if (manifest.max_turns !== undefined) request.max_turns = manifest.max_turns;
|
|
836
|
+
let route;
|
|
837
|
+
try {
|
|
838
|
+
route = preflightRequest(request);
|
|
839
|
+
} catch (error) {
|
|
840
|
+
throw failClosed(error?.message ?? "invalid route", "route_invalid");
|
|
841
|
+
}
|
|
842
|
+
if (!route.efforts.includes(effort)) {
|
|
843
|
+
throw failClosed(`${manifest.harness} does not accept effort ${effort}; allowed: ${route.efforts.join(", ")}`, "route_invalid");
|
|
844
|
+
}
|
|
845
|
+
let policy;
|
|
846
|
+
try {
|
|
847
|
+
policy = getRosterPolicy(deps);
|
|
848
|
+
} catch (error) {
|
|
849
|
+
throw failClosed(error?.message ?? "roster policy unavailable", "route_invalid");
|
|
850
|
+
}
|
|
851
|
+
assertAdmittedLineupRoute(manifest, role, policy);
|
|
852
|
+
return { role, route, effort };
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export function validateAssignmentManifest(manifest, deps = {}) {
|
|
856
|
+
const spawnSyncImpl = deps.spawnSync ?? spawnSync;
|
|
857
|
+
const exists = deps.existsSync ?? existsSync;
|
|
858
|
+
const read = deps.readFileSync ?? readFileSync;
|
|
859
|
+
const realpathImpl = deps.realpathSync ?? realpathSync;
|
|
860
|
+
const lstatImpl = deps.lstatSync ?? lstatSync;
|
|
861
|
+
const statImpl = deps.statSync ?? statSync;
|
|
862
|
+
const io = {
|
|
863
|
+
existsSync: exists,
|
|
864
|
+
readFileSync: read,
|
|
865
|
+
realpathSync: realpathImpl,
|
|
866
|
+
lstatSync: lstatImpl,
|
|
867
|
+
statSync: statImpl,
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
closedObject(manifest, ASSIGNMENT_KEYS, "assignment", OPTIONAL_ASSIGNMENT_KEYS);
|
|
871
|
+
if (manifest.schema !== ASSIGNMENT_SCHEMA) {
|
|
872
|
+
throw failClosed(`assignment schema must be ${ASSIGNMENT_SCHEMA}, got ${manifest.schema ?? "none"}`, "manifest_invalid");
|
|
873
|
+
}
|
|
874
|
+
const taskId = requireIdentity(manifest.task_id, "task_id");
|
|
875
|
+
const assignmentId = requireIdentity(manifest.assignment_id, "assignment_id");
|
|
876
|
+
if (!ASSIGNMENT_KINDS.includes(manifest.kind)) {
|
|
877
|
+
throw failClosed(`unknown kind ${manifest.kind}; expected ${ASSIGNMENT_KINDS.join(", ")}`, "route_invalid");
|
|
878
|
+
}
|
|
879
|
+
const cwd = resolvePath(requireNonemptyString(manifest.cwd, "cwd", "base_invalid"));
|
|
880
|
+
if (!exists(cwd)) {
|
|
881
|
+
throw failClosed(`cwd does not exist: ${cwd}`, "base_invalid");
|
|
882
|
+
}
|
|
883
|
+
const taskDir = validateTaskDir(manifest.task_dir, taskId, exists, statImpl, realpathImpl);
|
|
884
|
+
const { role, effort } = validateRoute(manifest, deps);
|
|
885
|
+
const gitState = inspectWorktree(cwd, spawnSyncImpl, realpathImpl);
|
|
886
|
+
const proven = validateBase(manifest.base, manifest.kind, gitState, spawnSyncImpl, cwd);
|
|
887
|
+
if (!isPlainObject(manifest.plan_ref) || !PLAN_REF_KINDS.includes(manifest.plan_ref.kind)) {
|
|
888
|
+
throw failClosed(`plan_ref.kind must be ${PLAN_REF_KINDS.join(" or ")}`, "plan_ref_invalid");
|
|
889
|
+
}
|
|
890
|
+
const planRef = manifest.plan_ref.kind === "current"
|
|
891
|
+
? validateCurrentPlanRef(manifest.plan_ref, taskDir, taskId, io)
|
|
892
|
+
: validateBootstrapPlanRef(manifest.plan_ref, taskDir, manifest.kind, io);
|
|
893
|
+
const inputs = validateInputs(manifest.inputs, cwd, io);
|
|
894
|
+
const contract = validateContract(manifest.contract, manifest.kind);
|
|
895
|
+
const requestedOutputDir = resolvePath(taskDir, requireNonemptyString(manifest.output_dir, "output_dir"));
|
|
896
|
+
const identityDir = assignmentRecordDir(taskDir, assignmentId);
|
|
897
|
+
if (lstatOrNull(identityDir, lstatImpl)) {
|
|
898
|
+
throw failClosed(`assignment_id ${assignmentId} is already present`, "identity_taken");
|
|
899
|
+
}
|
|
900
|
+
const compared = validateOutputDir(requestedOutputDir, taskDir, identityDir, cwd, lstatImpl, realpathImpl);
|
|
901
|
+
const reworkOf = validateRework(manifest.rework_of, taskDir, taskId, assignmentId, io);
|
|
902
|
+
|
|
903
|
+
const validated = {
|
|
904
|
+
schema: ASSIGNMENT_SCHEMA,
|
|
905
|
+
task_id: taskId,
|
|
906
|
+
assignment_id: assignmentId,
|
|
907
|
+
kind: manifest.kind,
|
|
908
|
+
role,
|
|
909
|
+
harness: manifest.harness,
|
|
910
|
+
model: manifest.model,
|
|
911
|
+
effort,
|
|
912
|
+
permission: manifest.permission,
|
|
913
|
+
cwd,
|
|
914
|
+
task_dir: taskDir,
|
|
915
|
+
record_dir: compared.record,
|
|
916
|
+
requested_output_dir: requestedOutputDir,
|
|
917
|
+
base: proven.base,
|
|
918
|
+
plan_ref: planRef,
|
|
919
|
+
inputs,
|
|
920
|
+
contract,
|
|
921
|
+
output_dir: compared.out,
|
|
922
|
+
git: Object.freeze({
|
|
923
|
+
head: gitState.head,
|
|
924
|
+
index_tree: proven.indexTree,
|
|
925
|
+
clean: gitState.clean,
|
|
926
|
+
}),
|
|
927
|
+
};
|
|
928
|
+
if (reworkOf !== undefined) validated.rework_of = reworkOf;
|
|
929
|
+
if (manifest.timeout_ms !== undefined) validated.timeout_ms = manifest.timeout_ms;
|
|
930
|
+
if (manifest.max_turns !== undefined) validated.max_turns = manifest.max_turns;
|
|
931
|
+
return Object.freeze(validated);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const COMPLETION_KEYS = Object.freeze([
|
|
935
|
+
"schema",
|
|
936
|
+
"task_id",
|
|
937
|
+
"assignment_id",
|
|
938
|
+
"rework_of",
|
|
939
|
+
"kind",
|
|
940
|
+
"role",
|
|
941
|
+
"manifest",
|
|
942
|
+
"prompt",
|
|
943
|
+
"plan",
|
|
944
|
+
"route",
|
|
945
|
+
"binding",
|
|
946
|
+
"invocation",
|
|
947
|
+
"candidate",
|
|
948
|
+
"recording",
|
|
949
|
+
"transport",
|
|
950
|
+
"usage",
|
|
951
|
+
"model_claim",
|
|
952
|
+
"sidecars",
|
|
953
|
+
"verification",
|
|
954
|
+
"critic",
|
|
955
|
+
"attribution",
|
|
956
|
+
]);
|
|
957
|
+
const REFUSAL_KEYS = Object.freeze([
|
|
958
|
+
"schema",
|
|
959
|
+
"task_id",
|
|
960
|
+
"assignment_id",
|
|
961
|
+
"kind",
|
|
962
|
+
"recordedAt",
|
|
963
|
+
"manifest",
|
|
964
|
+
"binding",
|
|
965
|
+
"stage",
|
|
966
|
+
"code",
|
|
967
|
+
"provider_calls",
|
|
968
|
+
"sidecars",
|
|
969
|
+
]);
|
|
970
|
+
const REFUSAL_STAGES = Object.freeze(["validation", "ownership", "prepare"]);
|
|
971
|
+
const MODEL_CLAIM_OVERRIDE_KEYS = Object.freeze([
|
|
972
|
+
"verification",
|
|
973
|
+
"cost",
|
|
974
|
+
"costUsd",
|
|
975
|
+
"cost_usd",
|
|
976
|
+
"acceptance",
|
|
977
|
+
"critic",
|
|
978
|
+
"role",
|
|
979
|
+
"usage",
|
|
980
|
+
"transport",
|
|
981
|
+
]);
|
|
982
|
+
function ioDeps(deps = {}) {
|
|
983
|
+
return {
|
|
984
|
+
spawnSync: deps.spawnSync ?? spawnSync,
|
|
985
|
+
existsSync: deps.existsSync ?? existsSync,
|
|
986
|
+
readFileSync: deps.readFileSync ?? readFileSync,
|
|
987
|
+
realpathSync: deps.realpathSync ?? realpathSync,
|
|
988
|
+
lstatSync: deps.lstatSync ?? lstatSync,
|
|
989
|
+
statSync: deps.statSync ?? statSync,
|
|
990
|
+
mkdirSync: deps.mkdirSync ?? mkdirSync,
|
|
991
|
+
writeFileSync: deps.writeFileSync ?? writeFileSync,
|
|
992
|
+
chmodSync: deps.chmodSync ?? chmodSync,
|
|
993
|
+
readdirSync: deps.readdirSync ?? readdirSync,
|
|
994
|
+
renameSync: deps.renameSync ?? renameSync,
|
|
995
|
+
rmdirSync: deps.rmdirSync ?? rmdirSync,
|
|
996
|
+
rmSync: deps.rmSync ?? rmSync,
|
|
997
|
+
manifestBytes: deps.manifestBytes,
|
|
998
|
+
now: deps.now,
|
|
999
|
+
rosterPolicy: deps.rosterPolicy,
|
|
1000
|
+
loadTrustedRosterPolicy: deps.loadTrustedRosterPolicy,
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function writePrivate(path, body, deps, flag = "wx") {
|
|
1005
|
+
const write = deps.writeFileSync ?? writeFileSync;
|
|
1006
|
+
const chmod = deps.chmodSync ?? chmodSync;
|
|
1007
|
+
const options = Buffer.isBuffer(body)
|
|
1008
|
+
? { mode: 0o600, flag }
|
|
1009
|
+
: { encoding: "utf8", mode: 0o600, flag };
|
|
1010
|
+
write(path, body, options);
|
|
1011
|
+
try {
|
|
1012
|
+
chmod(path, 0o600);
|
|
1013
|
+
} catch {
|
|
1014
|
+
// Windows cannot honor POSIX 0600; the create mode still ran.
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function originalManifestBytes(manifest, deps) {
|
|
1019
|
+
if (deps.manifestBytes) {
|
|
1020
|
+
return Buffer.isBuffer(deps.manifestBytes) ? deps.manifestBytes : Buffer.from(deps.manifestBytes);
|
|
1021
|
+
}
|
|
1022
|
+
return Buffer.from(JSON.stringify(manifest));
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function runnerErrorsPath(recordDir) {
|
|
1026
|
+
return join(recordDir, "runner-errors.jsonl");
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function appendRunnerError(recordDir, event, deps) {
|
|
1030
|
+
const path = runnerErrorsPath(recordDir);
|
|
1031
|
+
const line = `${JSON.stringify({
|
|
1032
|
+
at: new Date((deps.now ?? Date.now)()).toISOString(),
|
|
1033
|
+
step: event.step,
|
|
1034
|
+
code: event.code,
|
|
1035
|
+
message: String(event.message ?? ""),
|
|
1036
|
+
})}\n`;
|
|
1037
|
+
try {
|
|
1038
|
+
writePrivate(path, line, deps, "wx");
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
if (error && error.code === "EEXIST") {
|
|
1041
|
+
const write = deps.writeFileSync ?? writeFileSync;
|
|
1042
|
+
write(path, line, { encoding: "utf8", flag: "a", mode: 0o600 });
|
|
1043
|
+
} else {
|
|
1044
|
+
throw failClosed(`cannot write runner diagnostics: ${error?.message ?? error}`, "record_write_failed");
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
try {
|
|
1048
|
+
(deps.chmodSync ?? chmodSync)(path, 0o600);
|
|
1049
|
+
} catch {
|
|
1050
|
+
// Best-effort mode on platforms that ignore POSIX 0600.
|
|
1051
|
+
}
|
|
1052
|
+
const size = (deps.statSync ?? statSync)(path).size;
|
|
1053
|
+
return sidecarRef(path, size);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function sidecarRef(path, bytes) {
|
|
1057
|
+
return Object.freeze({ path, bytes });
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function bytesOf(body) {
|
|
1061
|
+
return Buffer.byteLength(typeof body === "string" ? body : Buffer.from(body), "utf8");
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const COUNT_DESCRIPTION = "Nonnegative integer. The runner caps public counts at 1000; JSON Schema minimum/maximum keywords are omitted because cross-provider keyword support is unproven.";
|
|
1065
|
+
|
|
1066
|
+
function closedSchemaObject(properties) {
|
|
1067
|
+
return {
|
|
1068
|
+
type: "object",
|
|
1069
|
+
additionalProperties: false,
|
|
1070
|
+
required: Object.keys(properties),
|
|
1071
|
+
properties,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
export function assignmentOutputSchema(kind) {
|
|
1076
|
+
if (REVIEW_KINDS.includes(kind)) {
|
|
1077
|
+
return closedSchemaObject({
|
|
1078
|
+
verdict: { type: "string", enum: [...REVIEW_VERDICTS] },
|
|
1079
|
+
summary: { type: "string" },
|
|
1080
|
+
findings: { type: "array", items: { type: "string" } },
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
return closedSchemaObject({
|
|
1084
|
+
status: { type: "string", enum: [...MODEL_CLAIM_STATUSES] },
|
|
1085
|
+
completedCount: { type: "integer", description: COUNT_DESCRIPTION },
|
|
1086
|
+
deferredCount: { type: "integer", description: COUNT_DESCRIPTION },
|
|
1087
|
+
artifactCount: { type: "integer", description: COUNT_DESCRIPTION },
|
|
1088
|
+
summary: { type: "string" },
|
|
1089
|
+
deferredItems: { type: "array", items: { type: "string" } },
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function planBody(validated, deps) {
|
|
1094
|
+
if (validated.plan_ref.kind !== "current") {
|
|
1095
|
+
return `No current plan pointer. Bootstrap reason: ${validated.plan_ref.reason}`;
|
|
1096
|
+
}
|
|
1097
|
+
const bytes = readRegularFile(
|
|
1098
|
+
validated.plan_ref.path,
|
|
1099
|
+
deps.readFileSync ?? readFileSync,
|
|
1100
|
+
deps.existsSync ?? existsSync,
|
|
1101
|
+
deps.statSync ?? statSync,
|
|
1102
|
+
);
|
|
1103
|
+
return bytes.toString("utf8");
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
export function renderAssignmentPrompt(validated, deps = {}) {
|
|
1107
|
+
const mechanism = validated.harness === "pi" && WRITER_KINDS.includes(validated.kind)
|
|
1108
|
+
? "Use repository tools for the authorized edit scope. This is an OpenRouter model through Pi, not a provider-native writer or long-lived worker."
|
|
1109
|
+
: READ_MECHANISMS[validated.harness];
|
|
1110
|
+
if (!mechanism) {
|
|
1111
|
+
throw failClosed(`no read mechanism for harness ${validated.harness}`, "route_invalid");
|
|
1112
|
+
}
|
|
1113
|
+
const review = REVIEW_KINDS.includes(validated.kind);
|
|
1114
|
+
const planText = planBody(validated, deps);
|
|
1115
|
+
const inputs = validated.inputs.map((item) => `- ${item.path} (${item.sha256})`).join("\n") || "- none";
|
|
1116
|
+
const deliverables = validated.contract.deliverables.map((item) => `- ${item}`).join("\n") || "- none";
|
|
1117
|
+
const deferred = validated.contract.deferred.map((item) => `- ${item}`).join("\n") || "- none";
|
|
1118
|
+
const rework = validated.rework_of ? `Rework of existing completion \`${validated.rework_of}\`.\n` : "";
|
|
1119
|
+
const verdictRule = review
|
|
1120
|
+
? "Review completion JSON requires a top-level `verdict` of exactly PASS or BLOCK. Never infer a verdict from prose."
|
|
1121
|
+
: "Do not supply a review verdict. Writer/planner completion JSON uses status/completedCount/deferredCount/artifactCount only.";
|
|
1122
|
+
return [
|
|
1123
|
+
`# KXM assignment ${validated.assignment_id}`,
|
|
1124
|
+
"",
|
|
1125
|
+
`Task \`${validated.task_id}\`. Assignment \`${validated.assignment_id}\`. Kind \`${validated.kind}\`. Role \`${validated.role}\`.`,
|
|
1126
|
+
rework,
|
|
1127
|
+
"## Candidate identity",
|
|
1128
|
+
"",
|
|
1129
|
+
`- cwd: ${validated.cwd}`,
|
|
1130
|
+
`- task_dir: ${validated.task_dir}`,
|
|
1131
|
+
`- HEAD: ${validated.git.head}`,
|
|
1132
|
+
`- index_tree: ${validated.git.index_tree}`,
|
|
1133
|
+
`- clean: ${validated.git.clean}`,
|
|
1134
|
+
`- base: ${validated.base.kind}${validated.base.kind === "staged" ? ` ${validated.base.index_tree}` : ` ${validated.base.commit}`}`,
|
|
1135
|
+
"",
|
|
1136
|
+
"## Current plan",
|
|
1137
|
+
"",
|
|
1138
|
+
validated.plan_ref.kind === "current"
|
|
1139
|
+
? `Pointer ${validated.plan_ref.pointer_path} generation ${validated.plan_ref.generation} sha256 ${validated.plan_ref.sha256}. Exact current plan follows.`
|
|
1140
|
+
: `Bootstrap: ${validated.plan_ref.reason}`,
|
|
1141
|
+
"",
|
|
1142
|
+
"```markdown",
|
|
1143
|
+
planText.replace(/```/g, "``\\`"),
|
|
1144
|
+
"```",
|
|
1145
|
+
"",
|
|
1146
|
+
"## Inputs",
|
|
1147
|
+
"",
|
|
1148
|
+
inputs,
|
|
1149
|
+
"",
|
|
1150
|
+
"## Contract",
|
|
1151
|
+
"",
|
|
1152
|
+
`Boundary: ${validated.contract.boundary}`,
|
|
1153
|
+
"",
|
|
1154
|
+
"Deliverables:",
|
|
1155
|
+
deliverables,
|
|
1156
|
+
"",
|
|
1157
|
+
`Witness id: ${validated.contract.witness.id}.`,
|
|
1158
|
+
WRITER_KINDS.includes(validated.kind)
|
|
1159
|
+
? "Run `npm run verify` before completing and report its exit code. Root re-runs the same fixed witness after you exit."
|
|
1160
|
+
: "Root runs the witness separately; do not run it.",
|
|
1161
|
+
"",
|
|
1162
|
+
"Deferred:",
|
|
1163
|
+
deferred,
|
|
1164
|
+
"",
|
|
1165
|
+
"## Read mechanism",
|
|
1166
|
+
"",
|
|
1167
|
+
mechanism,
|
|
1168
|
+
"",
|
|
1169
|
+
"## Completion requirements",
|
|
1170
|
+
"",
|
|
1171
|
+
verdictRule,
|
|
1172
|
+
"Return JSON matching the supplied output schema. Transport completion is not acceptance or verification.",
|
|
1173
|
+
"Do not set verification, critic, acceptance, role, usage, or cost fields; those are recorded from facts.",
|
|
1174
|
+
"When useful, leave concise private handoff notes (missing input, friction, what worked, next change, artifacts/checks tried) in the private summary; after exit they belong in `attribute` history. Notes never grant tools, waive verification, or count as human/hub approval.",
|
|
1175
|
+
"",
|
|
1176
|
+
].join("\n");
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function reserveIdentity(recordDir, assignmentId, manifestBytes, deps) {
|
|
1180
|
+
const mkdir = deps.mkdirSync ?? mkdirSync;
|
|
1181
|
+
try {
|
|
1182
|
+
mkdir(recordDir, { recursive: false, mode: 0o700 });
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
if (error && error.code === "EEXIST") {
|
|
1185
|
+
throw failClosed(`assignment_id ${assignmentId} is already present`, "identity_taken");
|
|
1186
|
+
}
|
|
1187
|
+
throw failClosed(`cannot create assignment identity: ${error?.message ?? error}`, "mkdir_failed");
|
|
1188
|
+
}
|
|
1189
|
+
const manifestPath = join(recordDir, "manifest.json");
|
|
1190
|
+
try {
|
|
1191
|
+
writePrivate(manifestPath, manifestBytes, deps, "wx");
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
if (error && error.runnerCode) throw error;
|
|
1194
|
+
throw failClosed(`cannot write identity manifest: ${error?.message ?? error}`, "record_write_failed");
|
|
1195
|
+
}
|
|
1196
|
+
return recordDir;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function reserveOutputDir(validated, deps) {
|
|
1200
|
+
if (validated.output_dir === validated.record_dir) return;
|
|
1201
|
+
const mkdir = deps.mkdirSync ?? mkdirSync;
|
|
1202
|
+
try {
|
|
1203
|
+
mkdir(dirname(validated.output_dir), { recursive: true, mode: 0o700 });
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
throw failClosed(`cannot create output_dir parent: ${error?.message ?? error}`, "mkdir_failed");
|
|
1206
|
+
}
|
|
1207
|
+
try {
|
|
1208
|
+
mkdir(validated.output_dir, { recursive: false, mode: 0o700 });
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
if (error && error.code === "EEXIST") {
|
|
1211
|
+
throw failClosed(`output_dir already exists: ${validated.output_dir}`, "output_dir_taken");
|
|
1212
|
+
}
|
|
1213
|
+
throw failClosed(`cannot create output_dir: ${error?.message ?? error}`, "mkdir_failed");
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function consumeOwnership(validated, manifest, deps) {
|
|
1218
|
+
reserveIdentity(
|
|
1219
|
+
validated.record_dir,
|
|
1220
|
+
validated.assignment_id,
|
|
1221
|
+
originalManifestBytes(manifest, deps),
|
|
1222
|
+
deps,
|
|
1223
|
+
);
|
|
1224
|
+
reserveOutputDir(validated, deps);
|
|
1225
|
+
return validated.record_dir;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function snapshotCandidate(cwd, spawnSyncImpl, realpathImpl) {
|
|
1229
|
+
try {
|
|
1230
|
+
const gitState = inspectWorktree(cwd, spawnSyncImpl, realpathImpl);
|
|
1231
|
+
const indexTree = git(cwd, ["write-tree"], spawnSyncImpl);
|
|
1232
|
+
const clean = gitState.statusRecords.every((record) => record.y === " " && !isUnmergedRecord(record));
|
|
1233
|
+
return Object.freeze({
|
|
1234
|
+
status: "recorded",
|
|
1235
|
+
head: gitState.head,
|
|
1236
|
+
index_tree: indexTree,
|
|
1237
|
+
clean,
|
|
1238
|
+
});
|
|
1239
|
+
} catch {
|
|
1240
|
+
return Object.freeze({ status: "unknown", code: "snapshot_failed" });
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function closedTransport(result, fallback) {
|
|
1245
|
+
if (!result || result.schema !== RESULT_SCHEMA) {
|
|
1246
|
+
return Object.freeze({
|
|
1247
|
+
status: "failed",
|
|
1248
|
+
stage: "run",
|
|
1249
|
+
ok: false,
|
|
1250
|
+
errorCode: "normalization_failed",
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
const status = TRANSPORT_STATUSES.includes(result.status) ? result.status : fallback.status;
|
|
1254
|
+
const stage = TRANSPORT_STAGES.includes(result.stage) ? result.stage : fallback.stage;
|
|
1255
|
+
let errorCode;
|
|
1256
|
+
if (typeof result.errorCode === "string") {
|
|
1257
|
+
errorCode = ERROR_CODES.includes(result.errorCode) ? result.errorCode : "unrecognized";
|
|
1258
|
+
}
|
|
1259
|
+
const transport = {
|
|
1260
|
+
status,
|
|
1261
|
+
stage,
|
|
1262
|
+
ok: result.ok === true && status === "completed",
|
|
1263
|
+
};
|
|
1264
|
+
if (typeof result?.startedAt === "string") transport.startedAt = result.startedAt;
|
|
1265
|
+
if (typeof result?.finishedAt === "string") transport.finishedAt = result.finishedAt;
|
|
1266
|
+
if (Number.isFinite(result?.latencyMs) && result.latencyMs >= 0) transport.latencyMs = result.latencyMs;
|
|
1267
|
+
if (Number.isInteger(result?.exitCode) || result?.exitCode === null) transport.exitCode = result.exitCode;
|
|
1268
|
+
if (typeof result?.timedOut === "boolean") transport.timedOut = result.timedOut;
|
|
1269
|
+
if (typeof result?.observedChildExit === "boolean") transport.observedChildExit = result.observedChildExit;
|
|
1270
|
+
if (typeof result?.signal === "string") transport.signal = result.signal;
|
|
1271
|
+
if (result?.killRequest && typeof result.killRequest === "object") {
|
|
1272
|
+
transport.killRequest = {
|
|
1273
|
+
signal: result.killRequest.signal,
|
|
1274
|
+
escalated: result.killRequest.escalated === true,
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
if (errorCode) transport.errorCode = errorCode;
|
|
1278
|
+
if (typeof result?.command === "string") transport.command = result.command;
|
|
1279
|
+
if (typeof result?.effectiveModel === "string") transport.effectiveModel = result.effectiveModel;
|
|
1280
|
+
return Object.freeze(transport);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function closedUsage(result) {
|
|
1284
|
+
const usage = {
|
|
1285
|
+
tokenBasis: TOKEN_BASIS,
|
|
1286
|
+
contextOccupancy: CONTEXT_OCCUPANCY_UNKNOWN,
|
|
1287
|
+
};
|
|
1288
|
+
const costBasis = COST_BASIS.includes(result?.costBasis) ? result.costBasis : "unknown";
|
|
1289
|
+
usage.costBasis = costBasis;
|
|
1290
|
+
const metadata = result?.providerMetadata && typeof result.providerMetadata === "object"
|
|
1291
|
+
? result.providerMetadata
|
|
1292
|
+
: {};
|
|
1293
|
+
for (const key of ["tokensIn", "tokensOut", "cacheReadTokens", "cacheCreationTokens", "reasoningTokens"]) {
|
|
1294
|
+
const value = result?.[key] ?? metadata[key];
|
|
1295
|
+
if (Number.isInteger(value) && value >= 0) usage[key] = value;
|
|
1296
|
+
}
|
|
1297
|
+
for (const key of ["costUsd", "providerReportedCostUsd"]) {
|
|
1298
|
+
const value = result?.[key];
|
|
1299
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) usage[key] = value;
|
|
1300
|
+
}
|
|
1301
|
+
if (result?.usagePartial === true) usage.usagePartial = true;
|
|
1302
|
+
return Object.freeze(usage);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function boundedModelClaim(result, kind) {
|
|
1306
|
+
const raw = result?.modelClaim;
|
|
1307
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
1308
|
+
const review = REVIEW_KINDS.includes(kind);
|
|
1309
|
+
const claim = {};
|
|
1310
|
+
if (typeof raw.status === "string") {
|
|
1311
|
+
claim.status = MODEL_CLAIM_STATUSES.includes(raw.status) ? raw.status : "unrecognized";
|
|
1312
|
+
}
|
|
1313
|
+
for (const key of ["completedCount", "deferredCount", "artifactCount", "unrecognizedCount", "bytes"]) {
|
|
1314
|
+
if (Number.isInteger(raw[key]) && raw[key] >= 0) claim[key] = raw[key];
|
|
1315
|
+
}
|
|
1316
|
+
if (review && REVIEW_VERDICTS.includes(raw.verdict)) claim.verdict = raw.verdict;
|
|
1317
|
+
if (typeof raw.path === "string") claim.path = raw.path;
|
|
1318
|
+
if (CLAIM_SOURCES.includes(raw.claimSource)) claim.claimSource = raw.claimSource;
|
|
1319
|
+
let unrecognized = claim.unrecognizedCount ?? 0;
|
|
1320
|
+
for (const key of Object.keys(raw)) {
|
|
1321
|
+
if (MODEL_CLAIM_OVERRIDE_KEYS.includes(key)) unrecognized += 1;
|
|
1322
|
+
}
|
|
1323
|
+
claim.unrecognizedCount = unrecognized;
|
|
1324
|
+
return Object.freeze(claim);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function deriveCritic(validated, candidate, modelClaim, transport) {
|
|
1328
|
+
if (!REVIEW_KINDS.includes(validated.kind)) {
|
|
1329
|
+
return Object.freeze({ kind: "none", reason: "not-review" });
|
|
1330
|
+
}
|
|
1331
|
+
if (!(transport?.status === "completed" && transport?.ok === true)) {
|
|
1332
|
+
return Object.freeze({ kind: "none", reason: "transport-not-completed" });
|
|
1333
|
+
}
|
|
1334
|
+
if (!candidate || candidate.status !== "recorded") {
|
|
1335
|
+
return Object.freeze({ kind: "none", reason: "candidate-unknown" });
|
|
1336
|
+
}
|
|
1337
|
+
const reviewedTree = validated.git.index_tree;
|
|
1338
|
+
const unchanged = candidate.head === validated.git.head
|
|
1339
|
+
&& candidate.index_tree === reviewedTree
|
|
1340
|
+
&& candidate.clean === validated.git.clean;
|
|
1341
|
+
if (!unchanged) return Object.freeze({ kind: "none", reason: "candidate-changed" });
|
|
1342
|
+
if (!modelClaim || !REVIEW_VERDICTS.includes(modelClaim.verdict)) {
|
|
1343
|
+
return Object.freeze({ kind: "none", reason: "no-verdict" });
|
|
1344
|
+
}
|
|
1345
|
+
return Object.freeze({
|
|
1346
|
+
kind: "review",
|
|
1347
|
+
verdict: modelClaim.verdict,
|
|
1348
|
+
judged_tree: reviewedTree,
|
|
1349
|
+
role: validated.role,
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function thrownHarnessResult(error, validated) {
|
|
1354
|
+
const explicit = error?.stage;
|
|
1355
|
+
const stage = explicit === "preflight" || explicit === "auth" ? explicit : "run";
|
|
1356
|
+
const errorCode = stage === "auth"
|
|
1357
|
+
? "auth_failed"
|
|
1358
|
+
: stage === "preflight"
|
|
1359
|
+
? "preflight_failed"
|
|
1360
|
+
: "unrecognized";
|
|
1361
|
+
return {
|
|
1362
|
+
schema: RESULT_SCHEMA,
|
|
1363
|
+
ok: false,
|
|
1364
|
+
status: "failed",
|
|
1365
|
+
stage,
|
|
1366
|
+
errorCode,
|
|
1367
|
+
harness: validated.harness,
|
|
1368
|
+
role: validated.role,
|
|
1369
|
+
requestedModel: validated.model,
|
|
1370
|
+
command: validated.harness,
|
|
1371
|
+
costBasis: "unknown",
|
|
1372
|
+
tokenBasis: TOKEN_BASIS,
|
|
1373
|
+
contextOccupancy: CONTEXT_OCCUPANCY_UNKNOWN,
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function routingInt(value) {
|
|
1378
|
+
if (!Number.isInteger(value) || value < 0) return undefined;
|
|
1379
|
+
if (value > 1_000_000) return undefined;
|
|
1380
|
+
return value;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function buildRoutingRecord(validated, promptSha, usage, transport, effectiveModel) {
|
|
1384
|
+
const planSha = validated.plan_ref?.kind === "current" ? validated.plan_ref.sha256 : undefined;
|
|
1385
|
+
const requestedModel = `${validated.harness}/${validated.model}`;
|
|
1386
|
+
const behavioral = {
|
|
1387
|
+
requestedModel,
|
|
1388
|
+
effectiveModel: effectiveModel ?? requestedModel,
|
|
1389
|
+
reasoningEffort: validated.effort,
|
|
1390
|
+
agentRole: validated.role,
|
|
1391
|
+
rolePromptSha256: promptSha,
|
|
1392
|
+
skills: [],
|
|
1393
|
+
};
|
|
1394
|
+
if (planSha) behavioral.workflowDefinitionSha256 = planSha;
|
|
1395
|
+
const providerMetadata = {
|
|
1396
|
+
harness: validated.harness,
|
|
1397
|
+
transportStatus: transport.status,
|
|
1398
|
+
costBasis: usage.costBasis,
|
|
1399
|
+
tokenBasis: usage.tokenBasis ?? TOKEN_BASIS,
|
|
1400
|
+
contextOccupancy: usage.contextOccupancy ?? CONTEXT_OCCUPANCY_UNKNOWN,
|
|
1401
|
+
};
|
|
1402
|
+
if (Number.isFinite(transport?.latencyMs) && transport.latencyMs >= 0) {
|
|
1403
|
+
providerMetadata.latencyMs = transport.latencyMs;
|
|
1404
|
+
}
|
|
1405
|
+
if (usage.usagePartial === true) providerMetadata.usagePartial = true;
|
|
1406
|
+
if (validated.rework_of) providerMetadata.rework_of = validated.rework_of;
|
|
1407
|
+
const record = {
|
|
1408
|
+
schema: ROUTING_RECORD_SCHEMA,
|
|
1409
|
+
behavioralHashVersion: BEHAVIORAL_HASH_VERSION,
|
|
1410
|
+
behavioralSha256: behavioralConfigHash(behavioral),
|
|
1411
|
+
workflowRunId: validated.assignment_id,
|
|
1412
|
+
stageId: validated.kind,
|
|
1413
|
+
attempt: 1,
|
|
1414
|
+
requestedModel,
|
|
1415
|
+
effectiveModel: behavioral.effectiveModel,
|
|
1416
|
+
reasoningEffort: validated.effort,
|
|
1417
|
+
agentRole: validated.role,
|
|
1418
|
+
rolePromptSha256: promptSha,
|
|
1419
|
+
skills: [],
|
|
1420
|
+
retries: validated.rework_of ? 1 : 0,
|
|
1421
|
+
transitions: 0,
|
|
1422
|
+
humanInterventions: 0,
|
|
1423
|
+
finalOutcome: "pending",
|
|
1424
|
+
providerMetadata,
|
|
1425
|
+
};
|
|
1426
|
+
if (planSha) record.workflowDefinitionSha256 = planSha;
|
|
1427
|
+
for (const key of ["tokensIn", "tokensOut", "cacheReadTokens"]) {
|
|
1428
|
+
const value = usage[key];
|
|
1429
|
+
if (!Number.isInteger(value) || value < 0) continue;
|
|
1430
|
+
const capped = routingInt(value);
|
|
1431
|
+
if (capped !== undefined) record[key] = capped;
|
|
1432
|
+
else providerMetadata[key] = value;
|
|
1433
|
+
}
|
|
1434
|
+
for (const key of ["cacheCreationTokens", "reasoningTokens"]) {
|
|
1435
|
+
const value = usage[key];
|
|
1436
|
+
if (Number.isInteger(value) && value >= 0) providerMetadata[key] = value;
|
|
1437
|
+
}
|
|
1438
|
+
if (usage.costBasis === "provider-reported") {
|
|
1439
|
+
const cost = typeof usage.costUsd === "number" ? usage.costUsd : usage.providerReportedCostUsd;
|
|
1440
|
+
if (typeof cost === "number" && Number.isFinite(cost) && cost >= 0) record.costUsd = cost;
|
|
1441
|
+
} else if (usage.costBasis === "unmetered") {
|
|
1442
|
+
const estimate = typeof usage.providerReportedCostUsd === "number"
|
|
1443
|
+
? usage.providerReportedCostUsd
|
|
1444
|
+
: usage.costUsd;
|
|
1445
|
+
if (typeof estimate === "number" && Number.isFinite(estimate) && estimate >= 0) {
|
|
1446
|
+
providerMetadata.unmeteredEstimateUsd = estimate;
|
|
1447
|
+
}
|
|
1448
|
+
} else if (usage.costBasis === "list") {
|
|
1449
|
+
const estimate = typeof usage.costUsd === "number" ? usage.costUsd : usage.providerReportedCostUsd;
|
|
1450
|
+
if (typeof estimate === "number" && Number.isFinite(estimate) && estimate >= 0) {
|
|
1451
|
+
providerMetadata.listEstimateUsd = estimate;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return parseRoutingRecord(record);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function routingRecordFromCompletion(completion) {
|
|
1458
|
+
return buildRoutingRecord(
|
|
1459
|
+
{
|
|
1460
|
+
assignment_id: completion.assignment_id,
|
|
1461
|
+
kind: completion.kind,
|
|
1462
|
+
harness: completion.route.harness,
|
|
1463
|
+
model: completion.route.model,
|
|
1464
|
+
effort: completion.route.effort,
|
|
1465
|
+
role: completion.route.role,
|
|
1466
|
+
plan_ref: completion.plan,
|
|
1467
|
+
rework_of: completion.rework_of,
|
|
1468
|
+
},
|
|
1469
|
+
completion.prompt.sha256,
|
|
1470
|
+
completion.usage,
|
|
1471
|
+
completion.transport,
|
|
1472
|
+
completion.transport?.effectiveModel,
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
function withoutRegeneratedTimestamps(value) {
|
|
1477
|
+
if (Array.isArray(value)) return value.map(withoutRegeneratedTimestamps);
|
|
1478
|
+
if (!value || typeof value !== "object") return value;
|
|
1479
|
+
const out = {};
|
|
1480
|
+
for (const key of Object.keys(value).sort()) {
|
|
1481
|
+
if (key === "recordedAt" || key === "createdAt" || key === "resolvedAt") continue;
|
|
1482
|
+
out[key] = withoutRegeneratedTimestamps(value[key]);
|
|
1483
|
+
}
|
|
1484
|
+
return out;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function sameStableFacts(left, right) {
|
|
1488
|
+
return JSON.stringify(withoutRegeneratedTimestamps(left)) === JSON.stringify(withoutRegeneratedTimestamps(right));
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function sameInvokedPath(left, right) {
|
|
1492
|
+
try {
|
|
1493
|
+
if (realpathSync(left) === realpathSync(right)) return true;
|
|
1494
|
+
} catch {
|
|
1495
|
+
// Fall through to inode identity when a parent alias cannot be resolved.
|
|
1496
|
+
}
|
|
1497
|
+
try {
|
|
1498
|
+
const leftStat = statSync(left);
|
|
1499
|
+
const rightStat = statSync(right);
|
|
1500
|
+
return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;
|
|
1501
|
+
} catch {
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
export function assignmentTelemetryPath(outputDir) {
|
|
1507
|
+
return join(outputDir, "telemetry.jsonl");
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function expectedTelemetryEvent(completion, routing) {
|
|
1511
|
+
const parsed = parseRoutingRecord(routing);
|
|
1512
|
+
const ok = completion.transport?.ok === true;
|
|
1513
|
+
return {
|
|
1514
|
+
schema: TELEMETRY_SCHEMA,
|
|
1515
|
+
recordedAt: nowIso(),
|
|
1516
|
+
host: "local",
|
|
1517
|
+
target: "cli",
|
|
1518
|
+
envelope: workerResult(
|
|
1519
|
+
agentWorker({
|
|
1520
|
+
name: `assignment:${completion.assignment_id}`,
|
|
1521
|
+
purpose: "dev assignment runner",
|
|
1522
|
+
model: completion.route.model,
|
|
1523
|
+
thinking: completion.route.effort,
|
|
1524
|
+
}),
|
|
1525
|
+
{
|
|
1526
|
+
command: "assignment-run",
|
|
1527
|
+
ok,
|
|
1528
|
+
outcome: "running",
|
|
1529
|
+
summary: `assignment ${completion.assignment_id} transport ${completion.transport.status}`,
|
|
1530
|
+
createdAt: nowIso(),
|
|
1531
|
+
routing: parsed,
|
|
1532
|
+
},
|
|
1533
|
+
),
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function readExistingTelemetryEvents(path, deps) {
|
|
1538
|
+
const read = deps.readFileSync ?? readFileSync;
|
|
1539
|
+
let raw;
|
|
1540
|
+
try {
|
|
1541
|
+
raw = read(path, "utf8");
|
|
1542
|
+
} catch (error) {
|
|
1543
|
+
throw failClosed(`cannot read assignment telemetry: ${error?.message ?? error}`, "telemetry_failed");
|
|
1544
|
+
}
|
|
1545
|
+
const lines = String(raw).split(/\r?\n/).filter((line) => line.trim() !== "");
|
|
1546
|
+
return lines.map((line) => {
|
|
1547
|
+
try {
|
|
1548
|
+
return JSON.parse(line);
|
|
1549
|
+
} catch {
|
|
1550
|
+
throw failClosed("existing telemetry is malformed", "telemetry_failed");
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
function assertTelemetryMatchesCompletion(path, expected, deps) {
|
|
1556
|
+
const events = readExistingTelemetryEvents(path, deps);
|
|
1557
|
+
if (events.length !== 1) {
|
|
1558
|
+
throw failClosed("telemetry.jsonl must contain exactly one bound event", "telemetry_failed");
|
|
1559
|
+
}
|
|
1560
|
+
const event = events[0];
|
|
1561
|
+
if (!isPlainObject(event) || event.schema !== TELEMETRY_SCHEMA) {
|
|
1562
|
+
throw failClosed("existing telemetry is not a bound assignment event", "telemetry_failed");
|
|
1563
|
+
}
|
|
1564
|
+
if (!sameStableFacts(event, expected)) {
|
|
1565
|
+
throw failClosed("existing telemetry does not match this assignment completion", "telemetry_failed");
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
export function appendAssignmentTelemetry(completion, outputDir, deps = {}) {
|
|
1570
|
+
const path = assignmentTelemetryPath(outputDir);
|
|
1571
|
+
const exists = deps.existsSync ?? existsSync;
|
|
1572
|
+
const routing = completion.routingRecord ?? routingRecordFromCompletion(completion);
|
|
1573
|
+
const expected = expectedTelemetryEvent(completion, routing);
|
|
1574
|
+
if (exists(path)) {
|
|
1575
|
+
assertTelemetryMatchesCompletion(path, expected, deps);
|
|
1576
|
+
return false;
|
|
1577
|
+
}
|
|
1578
|
+
const line = `${JSON.stringify(expected)}\n`;
|
|
1579
|
+
try {
|
|
1580
|
+
writePrivate(path, line, deps, "wx");
|
|
1581
|
+
return true;
|
|
1582
|
+
} catch (error) {
|
|
1583
|
+
if (error && error.code === "EEXIST") {
|
|
1584
|
+
assertTelemetryMatchesCompletion(path, expected, deps);
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
throw failClosed(`cannot append assignment telemetry: ${error?.message ?? error}`, "telemetry_failed");
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function writeCompletionExclusive(path, completion, deps) {
|
|
1592
|
+
const publicCompletion = pickPublicCompletion(completion);
|
|
1593
|
+
const body = `${JSON.stringify(publicCompletion, undefined, 2)}\n`;
|
|
1594
|
+
try {
|
|
1595
|
+
writePrivate(path, body, deps, "wx");
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
if (error && error.code === "EEXIST") {
|
|
1598
|
+
throw failClosed(`completion already exists: ${path}`, "record_write_failed");
|
|
1599
|
+
}
|
|
1600
|
+
throw failClosed(`cannot write completion: ${error?.message ?? error}`, "record_write_failed");
|
|
1601
|
+
}
|
|
1602
|
+
return publicCompletion;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function pickPublicCompletion(completion) {
|
|
1606
|
+
const picked = {};
|
|
1607
|
+
for (const key of COMPLETION_KEYS) {
|
|
1608
|
+
if (completion[key] !== undefined) picked[key] = completion[key];
|
|
1609
|
+
}
|
|
1610
|
+
return picked;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
function closeSidecars(entries) {
|
|
1614
|
+
const sidecars = {};
|
|
1615
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
1616
|
+
if (!value) continue;
|
|
1617
|
+
const path = value.path;
|
|
1618
|
+
const bytes = value.bytes;
|
|
1619
|
+
if (typeof path !== "string" || !Number.isInteger(bytes) || bytes < 0) continue;
|
|
1620
|
+
sidecars[key] = Object.freeze({ path, bytes });
|
|
1621
|
+
}
|
|
1622
|
+
return Object.freeze(sidecars);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
function recordedCandidate(candidate) {
|
|
1626
|
+
if (candidate && candidate.status === "recorded") return candidate;
|
|
1627
|
+
if (candidate && candidate.status === "unknown") return candidate;
|
|
1628
|
+
if (candidate && typeof candidate.head === "string") {
|
|
1629
|
+
return Object.freeze({
|
|
1630
|
+
status: "recorded",
|
|
1631
|
+
head: candidate.head,
|
|
1632
|
+
index_tree: candidate.index_tree,
|
|
1633
|
+
clean: candidate.clean === true,
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
return Object.freeze({ status: "unknown", code: "snapshot_failed" });
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
function buildCompletion(validated, hashes, candidate, harnessResult, sidecars, extras = {}) {
|
|
1640
|
+
const observed = recordedCandidate(candidate);
|
|
1641
|
+
const transport = closedTransport(harnessResult, {
|
|
1642
|
+
status: "failed",
|
|
1643
|
+
stage: "run",
|
|
1644
|
+
errorCode: "unrecognized",
|
|
1645
|
+
});
|
|
1646
|
+
const usage = closedUsage(harnessResult);
|
|
1647
|
+
const modelClaim = boundedModelClaim(harnessResult, validated.kind);
|
|
1648
|
+
const critic = deriveCritic(validated, observed, modelClaim, transport);
|
|
1649
|
+
const route = Object.freeze({
|
|
1650
|
+
harness: validated.harness,
|
|
1651
|
+
model: validated.model,
|
|
1652
|
+
effort: validated.effort,
|
|
1653
|
+
permission: validated.permission,
|
|
1654
|
+
role: validated.role,
|
|
1655
|
+
});
|
|
1656
|
+
const plan = validated.plan_ref.kind === "current"
|
|
1657
|
+
? Object.freeze({
|
|
1658
|
+
kind: "current",
|
|
1659
|
+
path: validated.plan_ref.path,
|
|
1660
|
+
sha256: validated.plan_ref.sha256,
|
|
1661
|
+
pointer_path: validated.plan_ref.pointer_path,
|
|
1662
|
+
generation: validated.plan_ref.generation,
|
|
1663
|
+
})
|
|
1664
|
+
: Object.freeze({ kind: "bootstrap", reason: validated.plan_ref.reason });
|
|
1665
|
+
const completion = {
|
|
1666
|
+
schema: COMPLETION_SCHEMA,
|
|
1667
|
+
task_id: validated.task_id,
|
|
1668
|
+
assignment_id: validated.assignment_id,
|
|
1669
|
+
kind: validated.kind,
|
|
1670
|
+
role: validated.role,
|
|
1671
|
+
manifest: Object.freeze({ sha256: hashes.manifestSha }),
|
|
1672
|
+
prompt: Object.freeze({ sha256: hashes.promptSha }),
|
|
1673
|
+
plan,
|
|
1674
|
+
route,
|
|
1675
|
+
binding: Object.freeze({
|
|
1676
|
+
task_dir: validated.task_dir,
|
|
1677
|
+
cwd: validated.cwd,
|
|
1678
|
+
record_dir: validated.record_dir,
|
|
1679
|
+
output_dir: validated.output_dir,
|
|
1680
|
+
}),
|
|
1681
|
+
invocation: extras.invocation === "thrown" ? "thrown" : "returned",
|
|
1682
|
+
candidate: observed,
|
|
1683
|
+
recording: extras.recording ?? Object.freeze({ status: "ok" }),
|
|
1684
|
+
transport,
|
|
1685
|
+
usage,
|
|
1686
|
+
sidecars: closeSidecars(sidecars),
|
|
1687
|
+
verification: Object.freeze({ status: "not-run" }),
|
|
1688
|
+
critic,
|
|
1689
|
+
attribution: Object.freeze({ status: "unclassified" }),
|
|
1690
|
+
};
|
|
1691
|
+
if (validated.rework_of) completion.rework_of = validated.rework_of;
|
|
1692
|
+
if (modelClaim) completion.model_claim = modelClaim;
|
|
1693
|
+
if (extras.routingRecord) completion.routingRecord = extras.routingRecord;
|
|
1694
|
+
return completion;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
function harnessRequest(validated, promptPath, schemaPath) {
|
|
1698
|
+
const request = {
|
|
1699
|
+
schema: REQUEST_SCHEMA,
|
|
1700
|
+
harness: validated.harness,
|
|
1701
|
+
role: validated.role,
|
|
1702
|
+
model: validated.model,
|
|
1703
|
+
permission: validated.permission,
|
|
1704
|
+
effort: validated.effort,
|
|
1705
|
+
prompt_file: promptPath,
|
|
1706
|
+
output_schema: schemaPath,
|
|
1707
|
+
cwd: validated.cwd,
|
|
1708
|
+
output_dir: validated.output_dir,
|
|
1709
|
+
};
|
|
1710
|
+
if (validated.timeout_ms !== undefined) request.timeout_ms = validated.timeout_ms;
|
|
1711
|
+
if (validated.max_turns !== undefined) request.max_turns = validated.max_turns;
|
|
1712
|
+
return request;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
function digestManifest(manifest, deps) {
|
|
1716
|
+
return sha256Bytes(originalManifestBytes(manifest, deps));
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
function identifyAssignment(manifest, io) {
|
|
1720
|
+
if (!isPlainObject(manifest)) return null;
|
|
1721
|
+
if (manifest.schema !== ASSIGNMENT_SCHEMA) return null;
|
|
1722
|
+
if (typeof manifest.task_id !== "string" || !IDENTITY.test(manifest.task_id)) return null;
|
|
1723
|
+
if (typeof manifest.assignment_id !== "string" || !IDENTITY.test(manifest.assignment_id)) return null;
|
|
1724
|
+
if (typeof manifest.task_dir !== "string" || !nodePath.isAbsolute(manifest.task_dir)) return null;
|
|
1725
|
+
try {
|
|
1726
|
+
const taskDir = validateTaskDir(
|
|
1727
|
+
manifest.task_dir,
|
|
1728
|
+
manifest.task_id,
|
|
1729
|
+
io.existsSync,
|
|
1730
|
+
io.statSync,
|
|
1731
|
+
io.realpathSync,
|
|
1732
|
+
);
|
|
1733
|
+
const recordDir = assignmentRecordDir(taskDir, manifest.assignment_id);
|
|
1734
|
+
const outputDir = typeof manifest.output_dir === "string" && manifest.output_dir.trim() !== ""
|
|
1735
|
+
? resolvePath(taskDir, manifest.output_dir)
|
|
1736
|
+
: undefined;
|
|
1737
|
+
return Object.freeze({
|
|
1738
|
+
task_id: manifest.task_id,
|
|
1739
|
+
assignment_id: manifest.assignment_id,
|
|
1740
|
+
task_dir: taskDir,
|
|
1741
|
+
record_dir: recordDir,
|
|
1742
|
+
output_dir: outputDir,
|
|
1743
|
+
kind: typeof manifest.kind === "string" ? manifest.kind : undefined,
|
|
1744
|
+
});
|
|
1745
|
+
} catch {
|
|
1746
|
+
return null;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
function refusalIdentity(validated) {
|
|
1751
|
+
return {
|
|
1752
|
+
task_id: validated.task_id,
|
|
1753
|
+
assignment_id: validated.assignment_id,
|
|
1754
|
+
task_dir: validated.task_dir,
|
|
1755
|
+
record_dir: validated.record_dir,
|
|
1756
|
+
output_dir: validated.requested_output_dir ?? validated.output_dir,
|
|
1757
|
+
kind: validated.kind,
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
function pickPublicRefusal(refusal) {
|
|
1762
|
+
const picked = {};
|
|
1763
|
+
for (const key of REFUSAL_KEYS) {
|
|
1764
|
+
if (refusal[key] !== undefined) picked[key] = refusal[key];
|
|
1765
|
+
}
|
|
1766
|
+
return picked;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
function writeRefusal(identity, manifest, stage, code, error, deps) {
|
|
1770
|
+
const io = ioDeps(deps);
|
|
1771
|
+
const recordDir = identity.record_dir;
|
|
1772
|
+
const runnerCode = RUNNER_CODES.includes(code) ? code : "unknown";
|
|
1773
|
+
const runnerErrors = appendRunnerError(recordDir, {
|
|
1774
|
+
step: stage,
|
|
1775
|
+
code: runnerCode,
|
|
1776
|
+
message: error?.message ?? runnerCode,
|
|
1777
|
+
}, { ...io, now: deps.now });
|
|
1778
|
+
const bytes = originalManifestBytes(manifest, deps);
|
|
1779
|
+
const refusal = {
|
|
1780
|
+
schema: REFUSAL_SCHEMA,
|
|
1781
|
+
task_id: identity.task_id,
|
|
1782
|
+
assignment_id: identity.assignment_id,
|
|
1783
|
+
recordedAt: new Date((deps.now ?? Date.now)()).toISOString(),
|
|
1784
|
+
manifest: Object.freeze({
|
|
1785
|
+
path: join(recordDir, "manifest.json"),
|
|
1786
|
+
sha256: sha256Bytes(bytes),
|
|
1787
|
+
}),
|
|
1788
|
+
binding: Object.freeze({
|
|
1789
|
+
task_dir: identity.task_dir,
|
|
1790
|
+
record_dir: recordDir,
|
|
1791
|
+
output_dir: identity.output_dir ?? recordDir,
|
|
1792
|
+
}),
|
|
1793
|
+
stage: REFUSAL_STAGES.includes(stage) ? stage : "validation",
|
|
1794
|
+
code: runnerCode,
|
|
1795
|
+
provider_calls: 0,
|
|
1796
|
+
sidecars: Object.freeze({
|
|
1797
|
+
runner_errors: runnerErrors,
|
|
1798
|
+
}),
|
|
1799
|
+
};
|
|
1800
|
+
if (ASSIGNMENT_KINDS.includes(identity.kind)) refusal.kind = identity.kind;
|
|
1801
|
+
writePrivate(
|
|
1802
|
+
join(recordDir, "refusal.json"),
|
|
1803
|
+
`${JSON.stringify(pickPublicRefusal(refusal), undefined, 2)}\n`,
|
|
1804
|
+
io,
|
|
1805
|
+
"wx",
|
|
1806
|
+
);
|
|
1807
|
+
return refusal;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
const RECORDING_STEP_CODES = Object.freeze({
|
|
1811
|
+
candidate_snapshot: "snapshot_failed",
|
|
1812
|
+
sidecars: "sidecar_failed",
|
|
1813
|
+
routing_record: "routing_record_failed",
|
|
1814
|
+
telemetry: "telemetry_failed",
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
function firstRecordingFailure(steps) {
|
|
1818
|
+
for (const step of ["candidate_snapshot", "sidecars", "routing_record", "telemetry"]) {
|
|
1819
|
+
if (steps[step] === "failed") {
|
|
1820
|
+
return { status: "failed", steps: Object.freeze({ ...steps }), code: RECORDING_STEP_CODES[step] };
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
return Object.freeze({ status: "ok" });
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
function trySidecarStat(path, bytes, io, recordDir, steps, now) {
|
|
1827
|
+
if (typeof path !== "string") return undefined;
|
|
1828
|
+
try {
|
|
1829
|
+
if (Number.isInteger(bytes) && bytes >= 0) return sidecarRef(path, bytes);
|
|
1830
|
+
if (!io.existsSync(path)) return undefined;
|
|
1831
|
+
return sidecarRef(path, io.statSync(path).size);
|
|
1832
|
+
} catch (error) {
|
|
1833
|
+
steps.sidecars = "failed";
|
|
1834
|
+
try {
|
|
1835
|
+
appendRunnerError(recordDir, {
|
|
1836
|
+
step: "sidecars",
|
|
1837
|
+
code: "sidecar_failed",
|
|
1838
|
+
message: error?.message ?? "sidecar stat failed",
|
|
1839
|
+
}, { ...io, now });
|
|
1840
|
+
} catch {
|
|
1841
|
+
// Diagnostics are best-effort; recording still records sidecar_failed.
|
|
1842
|
+
}
|
|
1843
|
+
return undefined;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
async function dispatchOwnedAssignment(validated, manifest, deps, ledger) {
|
|
1848
|
+
const io = ioDeps(deps);
|
|
1849
|
+
const recordDir = validated.record_dir;
|
|
1850
|
+
const promptPath = join(recordDir, "prompt.md");
|
|
1851
|
+
const schemaPath = join(recordDir, "output-schema.json");
|
|
1852
|
+
const preDispatchPath = join(recordDir, "pre-dispatch.json");
|
|
1853
|
+
const completionPath = join(recordDir, "completion.json");
|
|
1854
|
+
let promptText;
|
|
1855
|
+
try {
|
|
1856
|
+
promptText = renderAssignmentPrompt(validated, io);
|
|
1857
|
+
} catch (error) {
|
|
1858
|
+
throw failClosed(error?.message ?? "prompt render failed", "prompt_render_failed");
|
|
1859
|
+
}
|
|
1860
|
+
const schemaText = `${JSON.stringify(assignmentOutputSchema(validated.kind), undefined, 2)}\n`;
|
|
1861
|
+
try {
|
|
1862
|
+
writePrivate(promptPath, promptText, io, "wx");
|
|
1863
|
+
writePrivate(schemaPath, schemaText, io, "wx");
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
throw failClosed(error?.message ?? "cannot write prompt records", "record_write_failed");
|
|
1866
|
+
}
|
|
1867
|
+
const promptSha = sha256Bytes(Buffer.from(promptText));
|
|
1868
|
+
const manifestSha = digestManifest(manifest, deps);
|
|
1869
|
+
const planSha = validated.plan_ref.kind === "current" ? validated.plan_ref.sha256 : undefined;
|
|
1870
|
+
const preDispatch = {
|
|
1871
|
+
schema: ASSIGNMENT_DISPATCH_SCHEMA,
|
|
1872
|
+
task_id: validated.task_id,
|
|
1873
|
+
assignment_id: validated.assignment_id,
|
|
1874
|
+
timestamp: new Date((deps.now ?? Date.now)()).toISOString(),
|
|
1875
|
+
command: validated.harness,
|
|
1876
|
+
pid: null,
|
|
1877
|
+
manifest_sha256: manifestSha,
|
|
1878
|
+
prompt_sha256: promptSha,
|
|
1879
|
+
...(planSha ? { plan_sha256: planSha } : {}),
|
|
1880
|
+
};
|
|
1881
|
+
try {
|
|
1882
|
+
writePrivate(preDispatchPath, `${JSON.stringify(preDispatch)}\n`, io, "wx");
|
|
1883
|
+
} catch (error) {
|
|
1884
|
+
throw failClosed(error?.message ?? "cannot write pre-dispatch", "record_write_failed");
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
const run = deps.runHarness ?? runHarness;
|
|
1888
|
+
ledger.invoked = true;
|
|
1889
|
+
try {
|
|
1890
|
+
ledger.result = await run(harnessRequest(validated, promptPath, schemaPath), deps);
|
|
1891
|
+
ledger.invocation = "returned";
|
|
1892
|
+
} catch (error) {
|
|
1893
|
+
ledger.invocation = "thrown";
|
|
1894
|
+
ledger.result = thrownHarnessResult(error, validated);
|
|
1895
|
+
try {
|
|
1896
|
+
appendRunnerError(recordDir, {
|
|
1897
|
+
step: error?.stage === "preflight" || error?.stage === "auth" ? error.stage : "run",
|
|
1898
|
+
code: ledger.result.errorCode,
|
|
1899
|
+
message: error?.message ?? "native harness failed",
|
|
1900
|
+
}, { ...io, now: deps.now });
|
|
1901
|
+
} catch {
|
|
1902
|
+
// Diagnostics are best-effort after invocation; completion still records the throw.
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
const steps = {
|
|
1907
|
+
candidate_snapshot: "ok",
|
|
1908
|
+
sidecars: "ok",
|
|
1909
|
+
routing_record: "ok",
|
|
1910
|
+
telemetry: "ok",
|
|
1911
|
+
};
|
|
1912
|
+
const candidate = snapshotCandidate(validated.cwd, io.spawnSync, io.realpathSync);
|
|
1913
|
+
if (candidate.status === "unknown") {
|
|
1914
|
+
steps.candidate_snapshot = "failed";
|
|
1915
|
+
try {
|
|
1916
|
+
appendRunnerError(recordDir, {
|
|
1917
|
+
step: "candidate_snapshot",
|
|
1918
|
+
code: "snapshot_failed",
|
|
1919
|
+
message: "candidate snapshot failed",
|
|
1920
|
+
}, { ...io, now: deps.now });
|
|
1921
|
+
} catch {
|
|
1922
|
+
// Private diagnostics must not replace observed transport facts.
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
const hashes = { manifestSha, promptSha };
|
|
1927
|
+
const sidecars = {
|
|
1928
|
+
pre_dispatch: sidecarRef(preDispatchPath, bytesOf(`${JSON.stringify(preDispatch)}\n`)),
|
|
1929
|
+
prompt: sidecarRef(promptPath, bytesOf(promptText)),
|
|
1930
|
+
output_schema: sidecarRef(schemaPath, bytesOf(schemaText)),
|
|
1931
|
+
};
|
|
1932
|
+
const harnessResult = ledger.result;
|
|
1933
|
+
const dispatchCandidates = [
|
|
1934
|
+
harnessResult?.dispatchPath,
|
|
1935
|
+
join(validated.output_dir, "dispatch.json"),
|
|
1936
|
+
];
|
|
1937
|
+
for (const path of dispatchCandidates) {
|
|
1938
|
+
const ref = trySidecarStat(path, undefined, io, recordDir, steps, deps.now);
|
|
1939
|
+
if (ref) {
|
|
1940
|
+
sidecars.harness_dispatch = ref;
|
|
1941
|
+
break;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
const answer = trySidecarStat(harnessResult?.answerPath, harnessResult?.answerBytes, io, recordDir, steps, deps.now);
|
|
1945
|
+
if (answer) sidecars.answer = answer;
|
|
1946
|
+
const stderr = trySidecarStat(harnessResult?.stderrPath, harnessResult?.stderrBytes, io, recordDir, steps, deps.now);
|
|
1947
|
+
if (stderr) sidecars.stderr = stderr;
|
|
1948
|
+
const helperError = trySidecarStat(harnessResult?.errorPath, harnessResult?.errorBytes, io, recordDir, steps, deps.now);
|
|
1949
|
+
if (helperError) sidecars.error = helperError;
|
|
1950
|
+
const claimRef = trySidecarStat(
|
|
1951
|
+
harnessResult?.modelClaim?.path,
|
|
1952
|
+
harnessResult?.modelClaim?.bytes,
|
|
1953
|
+
io,
|
|
1954
|
+
recordDir,
|
|
1955
|
+
steps,
|
|
1956
|
+
deps.now,
|
|
1957
|
+
);
|
|
1958
|
+
if (claimRef) sidecars.model_claim = claimRef;
|
|
1959
|
+
const errorsPath = runnerErrorsPath(recordDir);
|
|
1960
|
+
const runnerErrors = trySidecarStat(errorsPath, undefined, io, recordDir, steps, deps.now);
|
|
1961
|
+
if (runnerErrors) sidecars.runner_errors = runnerErrors;
|
|
1962
|
+
|
|
1963
|
+
return recordInvocationCompletion(
|
|
1964
|
+
validated,
|
|
1965
|
+
hashes,
|
|
1966
|
+
candidate,
|
|
1967
|
+
harnessResult,
|
|
1968
|
+
sidecars,
|
|
1969
|
+
steps,
|
|
1970
|
+
ledger.invocation,
|
|
1971
|
+
io,
|
|
1972
|
+
deps.now,
|
|
1973
|
+
completionPath,
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
function writeRoutingRecordExclusive(path, routingRecord, io) {
|
|
1978
|
+
const body = `${JSON.stringify(routingRecord)}\n`;
|
|
1979
|
+
try {
|
|
1980
|
+
writePrivate(path, body, io, "wx");
|
|
1981
|
+
return body;
|
|
1982
|
+
} catch (error) {
|
|
1983
|
+
if (error && error.code === "EEXIST") {
|
|
1984
|
+
const existing = parseJsonFile(
|
|
1985
|
+
path,
|
|
1986
|
+
readRegularFile(path, io.readFileSync, io.existsSync, io.statSync, "routing_record_failed"),
|
|
1987
|
+
"routing record",
|
|
1988
|
+
"routing_record_failed",
|
|
1989
|
+
);
|
|
1990
|
+
const parsed = parseRoutingRecord(existing);
|
|
1991
|
+
if (!sameStableFacts(parsed, routingRecord)) {
|
|
1992
|
+
throw failClosed("existing routing record does not match this assignment completion", "routing_record_failed");
|
|
1993
|
+
}
|
|
1994
|
+
return `${JSON.stringify(existing)}\n`;
|
|
1995
|
+
}
|
|
1996
|
+
throw failClosed(`cannot write routing record: ${error?.message ?? error}`, "routing_record_failed");
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
function recordInvocationCompletion(
|
|
2001
|
+
validated,
|
|
2002
|
+
hashes,
|
|
2003
|
+
candidate,
|
|
2004
|
+
harnessResult,
|
|
2005
|
+
sidecars,
|
|
2006
|
+
steps,
|
|
2007
|
+
invocation,
|
|
2008
|
+
io,
|
|
2009
|
+
now,
|
|
2010
|
+
completionPath,
|
|
2011
|
+
) {
|
|
2012
|
+
const recordDir = validated.record_dir;
|
|
2013
|
+
const transport = closedTransport(harnessResult, {
|
|
2014
|
+
status: "failed",
|
|
2015
|
+
stage: "run",
|
|
2016
|
+
errorCode: "unrecognized",
|
|
2017
|
+
});
|
|
2018
|
+
const usage = closedUsage(harnessResult);
|
|
2019
|
+
let routingRecord;
|
|
2020
|
+
try {
|
|
2021
|
+
routingRecord = buildRoutingRecord(
|
|
2022
|
+
validated,
|
|
2023
|
+
hashes.promptSha,
|
|
2024
|
+
usage,
|
|
2025
|
+
transport,
|
|
2026
|
+
harnessResult?.effectiveModel,
|
|
2027
|
+
);
|
|
2028
|
+
const routingPath = join(recordDir, "routing-record.json");
|
|
2029
|
+
const routingBody = writeRoutingRecordExclusive(routingPath, routingRecord, io);
|
|
2030
|
+
sidecars.routing = sidecarRef(routingPath, bytesOf(routingBody));
|
|
2031
|
+
} catch (error) {
|
|
2032
|
+
steps.routing_record = "failed";
|
|
2033
|
+
steps.telemetry = "skipped";
|
|
2034
|
+
routingRecord = undefined;
|
|
2035
|
+
try {
|
|
2036
|
+
appendRunnerError(recordDir, {
|
|
2037
|
+
step: "routing_record",
|
|
2038
|
+
code: "routing_record_failed",
|
|
2039
|
+
message: error?.message ?? "routing record failed",
|
|
2040
|
+
}, { ...io, now });
|
|
2041
|
+
} catch {
|
|
2042
|
+
// Bookkeeping failure is recorded in completion.recording.
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
const completion = buildCompletion(validated, hashes, candidate, harnessResult, sidecars, {
|
|
2046
|
+
invocation,
|
|
2047
|
+
routingRecord,
|
|
2048
|
+
});
|
|
2049
|
+
completion.sidecars = closeSidecars(sidecars);
|
|
2050
|
+
if (steps.routing_record === "ok") {
|
|
2051
|
+
try {
|
|
2052
|
+
appendAssignmentTelemetry(completion, recordDir, io);
|
|
2053
|
+
} catch (error) {
|
|
2054
|
+
steps.telemetry = "failed";
|
|
2055
|
+
try {
|
|
2056
|
+
appendRunnerError(recordDir, {
|
|
2057
|
+
step: "telemetry",
|
|
2058
|
+
code: "telemetry_failed",
|
|
2059
|
+
message: error?.message ?? "telemetry append failed",
|
|
2060
|
+
}, { ...io, now });
|
|
2061
|
+
} catch {
|
|
2062
|
+
// Immutable completion still records telemetry_failed.
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
const errorsAfter = trySidecarStat(runnerErrorsPath(recordDir), undefined, io, recordDir, steps, now);
|
|
2067
|
+
if (errorsAfter) {
|
|
2068
|
+
sidecars.runner_errors = errorsAfter;
|
|
2069
|
+
completion.sidecars = closeSidecars(sidecars);
|
|
2070
|
+
}
|
|
2071
|
+
completion.recording = firstRecordingFailure(steps);
|
|
2072
|
+
return Object.freeze(writeCompletionExclusive(completionPath, completion, io));
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
export async function runAssignment(manifest, deps = {}) {
|
|
2076
|
+
const io = ioDeps(deps);
|
|
2077
|
+
const identity = identifyAssignment(manifest, io);
|
|
2078
|
+
let validated;
|
|
2079
|
+
try {
|
|
2080
|
+
validated = validateAssignmentManifest(manifest, deps);
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
const code = RUNNER_CODES.includes(error?.runnerCode) ? error.runnerCode : "unknown";
|
|
2083
|
+
if (!identity || code === "identity_taken") throw error;
|
|
2084
|
+
try {
|
|
2085
|
+
reserveIdentity(
|
|
2086
|
+
identity.record_dir,
|
|
2087
|
+
identity.assignment_id,
|
|
2088
|
+
originalManifestBytes(manifest, deps),
|
|
2089
|
+
io,
|
|
2090
|
+
);
|
|
2091
|
+
} catch (reserveError) {
|
|
2092
|
+
if (reserveError?.runnerCode === "identity_taken") throw reserveError;
|
|
2093
|
+
throw error;
|
|
2094
|
+
}
|
|
2095
|
+
writeRefusal(identity, manifest, "validation", code, error, deps);
|
|
2096
|
+
throw error;
|
|
2097
|
+
}
|
|
2098
|
+
try {
|
|
2099
|
+
consumeOwnership(validated, manifest, io);
|
|
2100
|
+
} catch (error) {
|
|
2101
|
+
const code = RUNNER_CODES.includes(error?.runnerCode) ? error.runnerCode : "unknown";
|
|
2102
|
+
if (code === "identity_taken") throw error;
|
|
2103
|
+
if (lstatOrNull(validated.record_dir, io.lstatSync)) {
|
|
2104
|
+
writeRefusal(refusalIdentity(validated), manifest, "ownership", code, error, deps);
|
|
2105
|
+
}
|
|
2106
|
+
throw error;
|
|
2107
|
+
}
|
|
2108
|
+
const ledger = { invoked: false, invocation: undefined, result: undefined };
|
|
2109
|
+
try {
|
|
2110
|
+
return await dispatchOwnedAssignment(validated, manifest, deps, ledger);
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
const ioCatch = ioDeps(deps);
|
|
2113
|
+
const completionPath = join(validated.record_dir, "completion.json");
|
|
2114
|
+
if (ioCatch.existsSync(completionPath)) throw error;
|
|
2115
|
+
if (!ledger.invoked) {
|
|
2116
|
+
const code = RUNNER_CODES.includes(error?.runnerCode) ? error.runnerCode : "unknown";
|
|
2117
|
+
writeRefusal(refusalIdentity(validated), manifest, "prepare", code, error, deps);
|
|
2118
|
+
throw error;
|
|
2119
|
+
}
|
|
2120
|
+
const candidate = snapshotCandidate(validated.cwd, ioCatch.spawnSync, ioCatch.realpathSync);
|
|
2121
|
+
const promptPath = join(validated.record_dir, "prompt.md");
|
|
2122
|
+
const promptSha = ioCatch.existsSync(promptPath)
|
|
2123
|
+
? sha256Bytes(ioCatch.readFileSync(promptPath))
|
|
2124
|
+
: sha256Bytes(Buffer.from(""));
|
|
2125
|
+
const harnessResult = ledger.invocation === "returned"
|
|
2126
|
+
? ledger.result
|
|
2127
|
+
: (ledger.result ?? thrownHarnessResult(error, validated));
|
|
2128
|
+
const steps = {
|
|
2129
|
+
candidate_snapshot: candidate.status === "recorded" ? "ok" : "failed",
|
|
2130
|
+
sidecars: "ok",
|
|
2131
|
+
routing_record: "ok",
|
|
2132
|
+
telemetry: "ok",
|
|
2133
|
+
};
|
|
2134
|
+
const sidecars = {};
|
|
2135
|
+
const dispatchCandidates = [
|
|
2136
|
+
harnessResult?.dispatchPath,
|
|
2137
|
+
join(validated.output_dir, "dispatch.json"),
|
|
2138
|
+
];
|
|
2139
|
+
for (const path of dispatchCandidates) {
|
|
2140
|
+
const ref = trySidecarStat(path, undefined, ioCatch, validated.record_dir, steps, deps.now);
|
|
2141
|
+
if (ref) {
|
|
2142
|
+
sidecars.harness_dispatch = ref;
|
|
2143
|
+
break;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
const answer = trySidecarStat(harnessResult?.answerPath, harnessResult?.answerBytes, ioCatch, validated.record_dir, steps, deps.now);
|
|
2147
|
+
if (answer) sidecars.answer = answer;
|
|
2148
|
+
const stderr = trySidecarStat(harnessResult?.stderrPath, harnessResult?.stderrBytes, ioCatch, validated.record_dir, steps, deps.now);
|
|
2149
|
+
if (stderr) sidecars.stderr = stderr;
|
|
2150
|
+
const helperError = trySidecarStat(harnessResult?.errorPath, harnessResult?.errorBytes, ioCatch, validated.record_dir, steps, deps.now);
|
|
2151
|
+
if (helperError) sidecars.error = helperError;
|
|
2152
|
+
return recordInvocationCompletion(
|
|
2153
|
+
validated,
|
|
2154
|
+
{ manifestSha: digestManifest(manifest, deps), promptSha },
|
|
2155
|
+
candidate,
|
|
2156
|
+
harnessResult,
|
|
2157
|
+
sidecars,
|
|
2158
|
+
steps,
|
|
2159
|
+
ledger.invocation === "returned" ? "returned" : "thrown",
|
|
2160
|
+
ioCatch,
|
|
2161
|
+
deps.now,
|
|
2162
|
+
completionPath,
|
|
2163
|
+
);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
function expectedRecordingResolution(completion, bytes, steps) {
|
|
2168
|
+
return {
|
|
2169
|
+
schema: RECORDING_RESOLUTION_SCHEMA,
|
|
2170
|
+
assignment_id: completion.assignment_id,
|
|
2171
|
+
task_id: completion.task_id,
|
|
2172
|
+
completion_sha256: sha256Bytes(bytes),
|
|
2173
|
+
steps,
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
function readExistingResolution(path, io, code = "record_write_failed") {
|
|
2178
|
+
return parseJsonFile(
|
|
2179
|
+
path,
|
|
2180
|
+
readRegularFile(path, io.readFileSync, io.existsSync, io.statSync, code),
|
|
2181
|
+
"recording resolution",
|
|
2182
|
+
code,
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
function assertResolutionMatches(existing, expected) {
|
|
2187
|
+
if (!isPlainObject(existing) || existing.schema !== RECORDING_RESOLUTION_SCHEMA) {
|
|
2188
|
+
throw failClosed("existing recording resolution is not bound to this completion", "record_write_failed");
|
|
2189
|
+
}
|
|
2190
|
+
if (!sameStableFacts(existing, expected)) {
|
|
2191
|
+
throw failClosed("existing recording resolution does not match this completion", "record_write_failed");
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
function writeRecordingResolved(recordDir, completion, bytes, steps, io) {
|
|
2196
|
+
const path = join(recordDir, "recording-resolved.json");
|
|
2197
|
+
const expected = expectedRecordingResolution(completion, bytes, steps);
|
|
2198
|
+
if (io.existsSync(path)) {
|
|
2199
|
+
assertResolutionMatches(readExistingResolution(path, io), expected);
|
|
2200
|
+
return false;
|
|
2201
|
+
}
|
|
2202
|
+
const body = `${JSON.stringify({
|
|
2203
|
+
...expected,
|
|
2204
|
+
resolvedAt: new Date((io.now ?? Date.now)()).toISOString(),
|
|
2205
|
+
}, undefined, 2)}\n`;
|
|
2206
|
+
try {
|
|
2207
|
+
writePrivate(path, body, io, "wx");
|
|
2208
|
+
return true;
|
|
2209
|
+
} catch (error) {
|
|
2210
|
+
if (error && error.code === "EEXIST") {
|
|
2211
|
+
assertResolutionMatches(readExistingResolution(path, io), expected);
|
|
2212
|
+
return false;
|
|
2213
|
+
}
|
|
2214
|
+
throw failClosed(`cannot write recording resolution: ${error?.message ?? error}`, "record_write_failed");
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
function originalRecordingSteps(completion) {
|
|
2219
|
+
if (completion.recording?.status === "ok") {
|
|
2220
|
+
return {
|
|
2221
|
+
candidate_snapshot: "ok",
|
|
2222
|
+
sidecars: "ok",
|
|
2223
|
+
routing_record: "ok",
|
|
2224
|
+
telemetry: "ok",
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
const steps = completion.recording?.steps;
|
|
2228
|
+
return {
|
|
2229
|
+
candidate_snapshot: steps?.candidate_snapshot
|
|
2230
|
+
?? (completion.candidate?.status === "recorded" ? "ok" : "failed"),
|
|
2231
|
+
sidecars: steps?.sidecars ?? "failed",
|
|
2232
|
+
routing_record: steps?.routing_record ?? "failed",
|
|
2233
|
+
telemetry: steps?.telemetry ?? "skipped",
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
function recoverSidecarStep(original) {
|
|
2238
|
+
// Observe recovers routing/telemetry only. Surviving recorded refs do not
|
|
2239
|
+
// prove an omitted failed sidecar (for example a missing answer ref) is ok.
|
|
2240
|
+
return original;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
function existingRoutingBytes(path, io) {
|
|
2244
|
+
return readRegularFile(path, io.readFileSync, io.existsSync, io.statSync, "routing_record_failed");
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
export async function observeAssignment(outputDir, deps = {}) {
|
|
2248
|
+
const io = ioDeps(deps);
|
|
2249
|
+
const completionPath = join(outputDir, "completion.json");
|
|
2250
|
+
const bytes = readRegularFile(completionPath, io.readFileSync, io.existsSync, io.statSync);
|
|
2251
|
+
const completion = parseJsonFile(completionPath, bytes, "assignment completion");
|
|
2252
|
+
if (!isPlainObject(completion) || completion.schema !== COMPLETION_SCHEMA) {
|
|
2253
|
+
throw failClosed(`assignment completion schema must be ${COMPLETION_SCHEMA}`, "record_write_failed");
|
|
2254
|
+
}
|
|
2255
|
+
if (completion.binding?.record_dir && !samePath(completion.binding.record_dir, outputDir, io.realpathSync)) {
|
|
2256
|
+
throw failClosed("completion is not bound to this record directory", "record_write_failed");
|
|
2257
|
+
}
|
|
2258
|
+
const steps = originalRecordingSteps(completion);
|
|
2259
|
+
if (completion.candidate?.status !== "recorded" && steps.candidate_snapshot === "ok") {
|
|
2260
|
+
steps.candidate_snapshot = "failed";
|
|
2261
|
+
}
|
|
2262
|
+
steps.sidecars = recoverSidecarStep(steps.sidecars);
|
|
2263
|
+
|
|
2264
|
+
const routingPath = join(outputDir, "routing-record.json");
|
|
2265
|
+
let routingRecord;
|
|
2266
|
+
let expectedRouting;
|
|
2267
|
+
try {
|
|
2268
|
+
expectedRouting = routingRecordFromCompletion(completion);
|
|
2269
|
+
} catch (error) {
|
|
2270
|
+
if (io.existsSync(routingPath)) {
|
|
2271
|
+
existingRoutingBytes(routingPath, io);
|
|
2272
|
+
throw failClosed("existing routing record cannot be validated against this completion", "routing_record_failed");
|
|
2273
|
+
}
|
|
2274
|
+
steps.routing_record = "failed";
|
|
2275
|
+
steps.telemetry = "skipped";
|
|
2276
|
+
writeRecordingResolved(outputDir, completion, bytes, steps, io);
|
|
2277
|
+
throw failClosed(error?.message ?? "cannot rebuild routing record", "routing_record_failed");
|
|
2278
|
+
}
|
|
2279
|
+
if (io.existsSync(routingPath)) {
|
|
2280
|
+
const existing = parseJsonFile(
|
|
2281
|
+
routingPath,
|
|
2282
|
+
existingRoutingBytes(routingPath, io),
|
|
2283
|
+
"routing record",
|
|
2284
|
+
"routing_record_failed",
|
|
2285
|
+
);
|
|
2286
|
+
let parsed;
|
|
2287
|
+
try {
|
|
2288
|
+
parsed = parseRoutingRecord(existing);
|
|
2289
|
+
} catch {
|
|
2290
|
+
throw failClosed("existing routing record is not a bound assignment record", "routing_record_failed");
|
|
2291
|
+
}
|
|
2292
|
+
if (parsed.workflowRunId !== completion.assignment_id) {
|
|
2293
|
+
throw failClosed("existing routing record is bound to a foreign assignment", "routing_record_failed");
|
|
2294
|
+
}
|
|
2295
|
+
if (!sameStableFacts(parsed, expectedRouting)) {
|
|
2296
|
+
throw failClosed("existing routing record does not match this assignment completion", "routing_record_failed");
|
|
2297
|
+
}
|
|
2298
|
+
routingRecord = parsed;
|
|
2299
|
+
steps.routing_record = "ok";
|
|
2300
|
+
} else {
|
|
2301
|
+
try {
|
|
2302
|
+
writeRoutingRecordExclusive(routingPath, expectedRouting, io);
|
|
2303
|
+
routingRecord = expectedRouting;
|
|
2304
|
+
steps.routing_record = "ok";
|
|
2305
|
+
} catch (error) {
|
|
2306
|
+
steps.routing_record = "failed";
|
|
2307
|
+
steps.telemetry = "skipped";
|
|
2308
|
+
throw failClosed(error?.message ?? "cannot rebuild routing record", "routing_record_failed");
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
let appended = false;
|
|
2312
|
+
if (steps.routing_record === "ok") {
|
|
2313
|
+
appended = appendAssignmentTelemetry({ ...completion, routingRecord }, outputDir, io);
|
|
2314
|
+
steps.telemetry = "ok";
|
|
2315
|
+
}
|
|
2316
|
+
writeRecordingResolved(outputDir, completion, bytes, steps, io);
|
|
2317
|
+
return appended;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
const CLI_USAGE = "usage: assignment-run.mjs run --manifest <absolute-path> | observe --record-dir <absolute-path> | witness --record-dir <absolute-path> | accept --task-dir <absolute-path> --commit <commit> --record-dir <absolute-path> --critic <absolute-path> --critic <absolute-path> [--observed-pr <id>] [--observed-ci <id>] | attribute --task-dir <absolute-path> --record-dir <absolute-path> --class <orchestration|model|environment|unclassified> --explanation-file <absolute-path> | observe-cost --task-dir <absolute-path> --input <absolute-path> | plan-current --task-dir <absolute-path> --plan <absolute-path> --sha256 <hex64> --base-commit <commit> --expected-generation <n> | change-report --task-dir <absolute-path>";
|
|
2321
|
+
|
|
2322
|
+
const WITNESS_RECEIPT_KEYS = Object.freeze([
|
|
2323
|
+
"schema",
|
|
2324
|
+
"receipt_id",
|
|
2325
|
+
"task_id",
|
|
2326
|
+
"assignment_id",
|
|
2327
|
+
"kind",
|
|
2328
|
+
"recordedAt",
|
|
2329
|
+
"manifest",
|
|
2330
|
+
"completion",
|
|
2331
|
+
"plan",
|
|
2332
|
+
"binding",
|
|
2333
|
+
"candidate",
|
|
2334
|
+
"gates",
|
|
2335
|
+
"result",
|
|
2336
|
+
"code",
|
|
2337
|
+
"startedAt",
|
|
2338
|
+
"finishedAt",
|
|
2339
|
+
"durationMs",
|
|
2340
|
+
]);
|
|
2341
|
+
const WITNESS_LATEST_KEYS = Object.freeze([
|
|
2342
|
+
"schema",
|
|
2343
|
+
"task_id",
|
|
2344
|
+
"assignment_id",
|
|
2345
|
+
"receipt_id",
|
|
2346
|
+
"path",
|
|
2347
|
+
"sha256",
|
|
2348
|
+
"recordedAt",
|
|
2349
|
+
"result",
|
|
2350
|
+
]);
|
|
2351
|
+
const WITNESS_DIRNAME = "witness";
|
|
2352
|
+
const WITNESS_HISTORY_DIRNAME = "history";
|
|
2353
|
+
const WITNESS_LATEST_FILENAME = "latest.json";
|
|
2354
|
+
|
|
2355
|
+
function witnessCode(code) {
|
|
2356
|
+
return WITNESS_CODES.includes(code) ? code : "unknown";
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
function isWitnessDirty(records) {
|
|
2360
|
+
return records.some((record) => isUnmergedRecord(record) || record.y !== " ");
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
function snapshotWitnessCandidate(cwd, spawnSyncImpl, realpathImpl) {
|
|
2364
|
+
try {
|
|
2365
|
+
const gitState = inspectWorktree(cwd, spawnSyncImpl, realpathImpl);
|
|
2366
|
+
const { result, stdout } = runGit(cwd, ["write-tree"], spawnSyncImpl);
|
|
2367
|
+
if (result.status !== 0) {
|
|
2368
|
+
return Object.freeze({ status: "unknown", code: "snapshot_failed" });
|
|
2369
|
+
}
|
|
2370
|
+
const indexTree = stdout.trim();
|
|
2371
|
+
if (!HEX40.test(indexTree)) {
|
|
2372
|
+
return Object.freeze({ status: "unknown", code: "snapshot_failed" });
|
|
2373
|
+
}
|
|
2374
|
+
return Object.freeze({
|
|
2375
|
+
status: "recorded",
|
|
2376
|
+
head: gitState.head,
|
|
2377
|
+
index_tree: indexTree,
|
|
2378
|
+
clean: !isWitnessDirty(gitState.statusRecords),
|
|
2379
|
+
});
|
|
2380
|
+
} catch {
|
|
2381
|
+
return Object.freeze({ status: "unknown", code: "snapshot_failed" });
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
function recordedSnapshot(snapshot) {
|
|
2386
|
+
if (snapshot?.status === "recorded") {
|
|
2387
|
+
return Object.freeze({
|
|
2388
|
+
head: snapshot.head,
|
|
2389
|
+
index_tree: snapshot.index_tree,
|
|
2390
|
+
clean: snapshot.clean === true,
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
return Object.freeze({ status: "unknown", code: snapshot?.code === "snapshot_failed" ? "snapshot_failed" : "unknown" });
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
function witnessDir(recordDir) {
|
|
2397
|
+
return join(recordDir, WITNESS_DIRNAME);
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
function witnessHistoryDir(recordDir) {
|
|
2401
|
+
return join(witnessDir(recordDir), WITNESS_HISTORY_DIRNAME);
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
function witnessLatestPath(recordDir) {
|
|
2405
|
+
return join(witnessDir(recordDir), WITNESS_LATEST_FILENAME);
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
function nextWitnessReceiptId(historyDir, now, existsImpl) {
|
|
2409
|
+
const iso = new Date(now()).toISOString().replace(/[-:.]/g, "");
|
|
2410
|
+
let id = `w-${iso}`;
|
|
2411
|
+
let n = 2;
|
|
2412
|
+
while (existsImpl(join(historyDir, `${id}.json`))) {
|
|
2413
|
+
id = `w-${iso}-${n}`;
|
|
2414
|
+
n += 1;
|
|
2415
|
+
}
|
|
2416
|
+
return id;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
function loadWitnessCompletion(recordDir, io) {
|
|
2420
|
+
const completionPath = join(recordDir, "completion.json");
|
|
2421
|
+
if (!io.existsSync(completionPath)) {
|
|
2422
|
+
throw failClosed(`missing assignment completion: ${completionPath}`, "completion_missing");
|
|
2423
|
+
}
|
|
2424
|
+
const bytes = readRegularFile(completionPath, io.readFileSync, io.existsSync, io.statSync, "completion_invalid");
|
|
2425
|
+
const completion = parseJsonFile(completionPath, bytes, "assignment completion", "completion_invalid");
|
|
2426
|
+
if (!isPlainObject(completion) || completion.schema !== COMPLETION_SCHEMA) {
|
|
2427
|
+
throw failClosed(`assignment completion schema must be ${COMPLETION_SCHEMA}`, "completion_invalid");
|
|
2428
|
+
}
|
|
2429
|
+
if (completion.binding?.record_dir && !samePath(completion.binding.record_dir, recordDir, io.realpathSync)) {
|
|
2430
|
+
throw failClosed("completion is not bound to this record directory", "witness_binding_invalid");
|
|
2431
|
+
}
|
|
2432
|
+
return { completion, bytes, path: completionPath };
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
function loadStoredManifest(recordDir, completion, io) {
|
|
2436
|
+
const path = join(recordDir, "manifest.json");
|
|
2437
|
+
const bytes = readRegularFile(path, io.readFileSync, io.existsSync, io.statSync, "witness_binding_invalid");
|
|
2438
|
+
const sha = sha256Bytes(bytes);
|
|
2439
|
+
if (typeof completion.manifest?.sha256 !== "string" || sha !== completion.manifest.sha256) {
|
|
2440
|
+
throw failClosed("stored manifest digest does not match completion.manifest.sha256", "witness_binding_invalid");
|
|
2441
|
+
}
|
|
2442
|
+
const manifest = parseJsonFile(path, bytes, "stored assignment manifest", "witness_binding_invalid");
|
|
2443
|
+
return { manifest, path, sha256: sha };
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
function bindWitnessIdentityFromCompletion(completion, recordDir, io) {
|
|
2447
|
+
const taskId = requireIdentity(completion.task_id, "completion.task_id", "witness_binding_invalid");
|
|
2448
|
+
const assignmentId = requireIdentity(completion.assignment_id, "completion.assignment_id", "witness_binding_invalid");
|
|
2449
|
+
const binding = closedObject(
|
|
2450
|
+
completion.binding,
|
|
2451
|
+
["task_dir", "cwd", "record_dir", "output_dir"],
|
|
2452
|
+
"completion.binding",
|
|
2453
|
+
[],
|
|
2454
|
+
"witness_binding_invalid",
|
|
2455
|
+
);
|
|
2456
|
+
const taskDir = validateTaskDir(binding.task_dir, taskId, io.existsSync, io.statSync, io.realpathSync);
|
|
2457
|
+
if (!samePath(binding.record_dir, recordDir, io.realpathSync)) {
|
|
2458
|
+
throw failClosed("completion.binding.record_dir does not match --record-dir", "witness_binding_invalid");
|
|
2459
|
+
}
|
|
2460
|
+
const cwd = resolvePath(requireNonemptyString(binding.cwd, "completion.binding.cwd", "witness_binding_invalid"));
|
|
2461
|
+
if (!io.existsSync(cwd)) {
|
|
2462
|
+
throw failClosed(`cwd does not exist: ${cwd}`, "witness_binding_invalid");
|
|
2463
|
+
}
|
|
2464
|
+
return Object.freeze({
|
|
2465
|
+
task_id: taskId,
|
|
2466
|
+
assignment_id: assignmentId,
|
|
2467
|
+
kind: typeof completion.kind === "string" ? completion.kind : undefined,
|
|
2468
|
+
task_dir: taskDir,
|
|
2469
|
+
cwd,
|
|
2470
|
+
record_dir: comparablePath(recordDir, io.realpathSync, io.lstatSync),
|
|
2471
|
+
output_dir: resolvePath(binding.output_dir),
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
function validateStoredManifestBinding(manifest, recordDir, io) {
|
|
2476
|
+
closedObject(manifest, ASSIGNMENT_KEYS, "stored assignment manifest", OPTIONAL_ASSIGNMENT_KEYS, "witness_binding_invalid");
|
|
2477
|
+
if (manifest.schema !== ASSIGNMENT_SCHEMA) {
|
|
2478
|
+
throw failClosed(`stored assignment schema must be ${ASSIGNMENT_SCHEMA}`, "witness_binding_invalid");
|
|
2479
|
+
}
|
|
2480
|
+
const taskId = requireIdentity(manifest.task_id, "stored task_id", "witness_binding_invalid");
|
|
2481
|
+
const assignmentId = requireIdentity(manifest.assignment_id, "stored assignment_id", "witness_binding_invalid");
|
|
2482
|
+
if (!ASSIGNMENT_KINDS.includes(manifest.kind)) {
|
|
2483
|
+
throw failClosed(`unknown stored kind ${manifest.kind}`, "witness_binding_invalid");
|
|
2484
|
+
}
|
|
2485
|
+
const cwd = resolvePath(requireNonemptyString(manifest.cwd, "stored cwd", "witness_binding_invalid"));
|
|
2486
|
+
if (!io.existsSync(cwd)) {
|
|
2487
|
+
throw failClosed(`stored cwd does not exist: ${cwd}`, "witness_binding_invalid");
|
|
2488
|
+
}
|
|
2489
|
+
const taskDir = validateTaskDir(manifest.task_dir, taskId, io.existsSync, io.statSync, io.realpathSync);
|
|
2490
|
+
const { role, effort } = validateRoute(manifest, io);
|
|
2491
|
+
const contract = validateContract(manifest.contract, manifest.kind);
|
|
2492
|
+
const requestedOutputDir = resolvePath(taskDir, requireNonemptyString(manifest.output_dir, "stored output_dir", "witness_binding_invalid"));
|
|
2493
|
+
const identityDir = assignmentRecordDir(taskDir, assignmentId);
|
|
2494
|
+
if (!samePath(identityDir, recordDir, io.realpathSync)) {
|
|
2495
|
+
throw failClosed("canonical record path must be task_dir/assignment_id", "witness_binding_invalid");
|
|
2496
|
+
}
|
|
2497
|
+
const compared = comparableOutputBinding(
|
|
2498
|
+
requestedOutputDir,
|
|
2499
|
+
taskDir,
|
|
2500
|
+
identityDir,
|
|
2501
|
+
cwd,
|
|
2502
|
+
io.lstatSync,
|
|
2503
|
+
io.realpathSync,
|
|
2504
|
+
);
|
|
2505
|
+
if (!isPlainObject(manifest.plan_ref) || !PLAN_REF_KINDS.includes(manifest.plan_ref.kind)) {
|
|
2506
|
+
throw failClosed(`stored plan_ref.kind must be ${PLAN_REF_KINDS.join(" or ")}`, "plan_mismatch");
|
|
2507
|
+
}
|
|
2508
|
+
const planRef = manifest.plan_ref.kind === "current"
|
|
2509
|
+
? validateCurrentPlanRef(manifest.plan_ref, taskDir, taskId, io)
|
|
2510
|
+
: validateBootstrapPlanRef(manifest.plan_ref, taskDir, manifest.kind, io);
|
|
2511
|
+
const storedBase = closedObject(
|
|
2512
|
+
manifest.base,
|
|
2513
|
+
["kind", "commit", "index_tree"],
|
|
2514
|
+
"stored base",
|
|
2515
|
+
["index_tree"],
|
|
2516
|
+
"witness_binding_invalid",
|
|
2517
|
+
);
|
|
2518
|
+
if (!BASE_KINDS.includes(storedBase.kind)) {
|
|
2519
|
+
throw failClosed(`unknown stored base.kind ${storedBase.kind}`, "witness_binding_invalid");
|
|
2520
|
+
}
|
|
2521
|
+
const baseCommit = requireHex(storedBase.commit, "stored base.commit", HEX40, "witness_binding_invalid");
|
|
2522
|
+
const base = storedBase.kind === "staged"
|
|
2523
|
+
? Object.freeze({
|
|
2524
|
+
kind: "staged",
|
|
2525
|
+
commit: baseCommit,
|
|
2526
|
+
index_tree: requireHex(storedBase.index_tree, "stored base.index_tree", HEX40, "witness_binding_invalid"),
|
|
2527
|
+
})
|
|
2528
|
+
: Object.freeze({ kind: "clean", commit: baseCommit });
|
|
2529
|
+
const reworkOf = manifest.rework_of === undefined
|
|
2530
|
+
? undefined
|
|
2531
|
+
: requireIdentity(manifest.rework_of, "stored rework_of", "witness_binding_invalid");
|
|
2532
|
+
return Object.freeze({
|
|
2533
|
+
task_id: taskId,
|
|
2534
|
+
assignment_id: assignmentId,
|
|
2535
|
+
kind: manifest.kind,
|
|
2536
|
+
role,
|
|
2537
|
+
harness: manifest.harness,
|
|
2538
|
+
model: manifest.model,
|
|
2539
|
+
effort,
|
|
2540
|
+
permission: manifest.permission,
|
|
2541
|
+
cwd,
|
|
2542
|
+
task_dir: taskDir,
|
|
2543
|
+
record_dir: comparablePath(recordDir, io.realpathSync, io.lstatSync),
|
|
2544
|
+
output_dir: compared.out,
|
|
2545
|
+
plan_ref: planRef,
|
|
2546
|
+
contract,
|
|
2547
|
+
base,
|
|
2548
|
+
...(reworkOf ? { rework_of: reworkOf } : {}),
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
function assertCompletionMatchesStored(completion, stored, io) {
|
|
2553
|
+
if (completion.task_id !== stored.task_id) {
|
|
2554
|
+
throw failClosed("completion.task_id does not match stored manifest", "witness_binding_invalid");
|
|
2555
|
+
}
|
|
2556
|
+
if (completion.assignment_id !== stored.assignment_id) {
|
|
2557
|
+
throw failClosed("completion.assignment_id does not match stored manifest", "witness_binding_invalid");
|
|
2558
|
+
}
|
|
2559
|
+
if (completion.kind !== stored.kind) {
|
|
2560
|
+
throw failClosed("completion.kind does not match stored manifest", "witness_binding_invalid");
|
|
2561
|
+
}
|
|
2562
|
+
const route = closedObject(
|
|
2563
|
+
completion.route,
|
|
2564
|
+
["harness", "model", "effort", "permission", "role"],
|
|
2565
|
+
"completion.route",
|
|
2566
|
+
[],
|
|
2567
|
+
"witness_binding_invalid",
|
|
2568
|
+
);
|
|
2569
|
+
if (
|
|
2570
|
+
route.harness !== stored.harness
|
|
2571
|
+
|| route.model !== stored.model
|
|
2572
|
+
|| route.effort !== stored.effort
|
|
2573
|
+
|| route.permission !== stored.permission
|
|
2574
|
+
|| route.role !== stored.role
|
|
2575
|
+
) {
|
|
2576
|
+
throw failClosed("completion.route does not match stored manifest", "witness_binding_invalid");
|
|
2577
|
+
}
|
|
2578
|
+
const binding = closedObject(
|
|
2579
|
+
completion.binding,
|
|
2580
|
+
["task_dir", "cwd", "record_dir", "output_dir"],
|
|
2581
|
+
"completion.binding",
|
|
2582
|
+
[],
|
|
2583
|
+
"witness_binding_invalid",
|
|
2584
|
+
);
|
|
2585
|
+
if (!samePath(binding.task_dir, stored.task_dir, io.realpathSync)) {
|
|
2586
|
+
throw failClosed("completion.binding.task_dir does not match stored manifest", "witness_binding_invalid");
|
|
2587
|
+
}
|
|
2588
|
+
if (!samePath(binding.cwd, stored.cwd, io.realpathSync)) {
|
|
2589
|
+
throw failClosed("completion.binding.cwd does not match stored manifest", "witness_binding_invalid");
|
|
2590
|
+
}
|
|
2591
|
+
if (!samePath(binding.record_dir, stored.record_dir, io.realpathSync)) {
|
|
2592
|
+
throw failClosed("completion.binding.record_dir does not match stored manifest", "witness_binding_invalid");
|
|
2593
|
+
}
|
|
2594
|
+
if (!samePath(binding.output_dir, stored.output_dir, io.realpathSync)) {
|
|
2595
|
+
throw failClosed("completion.binding.output_dir does not match stored manifest", "witness_binding_invalid");
|
|
2596
|
+
}
|
|
2597
|
+
if ((completion.rework_of ?? undefined) !== (stored.rework_of ?? undefined)) {
|
|
2598
|
+
throw failClosed("completion.rework_of does not match stored manifest", "witness_binding_invalid");
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
function bindWitnessPlan(completion, stored, io) {
|
|
2603
|
+
const storedPlan = stored.plan_ref;
|
|
2604
|
+
const plan = completion.plan;
|
|
2605
|
+
if (storedPlan.kind === "current") {
|
|
2606
|
+
if (plan?.kind === "bootstrap") {
|
|
2607
|
+
throw failClosed("stored current manifest forbids bootstrap completion", "plan_mismatch");
|
|
2608
|
+
}
|
|
2609
|
+
if (plan?.kind !== "current") {
|
|
2610
|
+
throw failClosed("completion.plan.kind must be current", "plan_mismatch");
|
|
2611
|
+
}
|
|
2612
|
+
const claimed = requireHex(plan.sha256, "completion.plan.sha256", HEX64, "plan_mismatch");
|
|
2613
|
+
if (claimed !== storedPlan.sha256) {
|
|
2614
|
+
throw failClosed("completion.plan.sha256 does not match stored current plan", "plan_mismatch");
|
|
2615
|
+
}
|
|
2616
|
+
if (!samePath(plan.path, storedPlan.path, io.realpathSync)) {
|
|
2617
|
+
throw failClosed("completion.plan.path does not match stored current plan", "plan_mismatch");
|
|
2618
|
+
}
|
|
2619
|
+
return storedPlan;
|
|
2620
|
+
}
|
|
2621
|
+
if (plan?.kind !== "bootstrap") {
|
|
2622
|
+
throw failClosed("bootstrap stored manifest requires bootstrap completion.plan", "plan_mismatch");
|
|
2623
|
+
}
|
|
2624
|
+
requireNonemptyString(plan.reason, "completion.plan.reason", "plan_mismatch");
|
|
2625
|
+
return storedPlan;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
function recordingBookkeepingReady(steps) {
|
|
2629
|
+
return steps?.routing_record === "ok" && steps?.telemetry === "ok";
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
function assertWitnessRecordingReady(completion, bytes, recordDir, io) {
|
|
2633
|
+
const recording = completion.recording;
|
|
2634
|
+
if (!isPlainObject(recording)) {
|
|
2635
|
+
throw failClosed("completion.recording is required", "recording_unresolved");
|
|
2636
|
+
}
|
|
2637
|
+
if (recording.status === "ok") return;
|
|
2638
|
+
if (recording.status !== "failed") {
|
|
2639
|
+
throw failClosed("completion.recording.status must be ok or failed", "recording_unresolved");
|
|
2640
|
+
}
|
|
2641
|
+
const original = originalRecordingSteps(completion);
|
|
2642
|
+
if (recordingBookkeepingReady(original)) return;
|
|
2643
|
+
const resolutionPath = join(recordDir, "recording-resolved.json");
|
|
2644
|
+
if (!io.existsSync(resolutionPath)) {
|
|
2645
|
+
throw failClosed("unresolved routing or telemetry recording must be recovered before witness", "recording_unresolved");
|
|
2646
|
+
}
|
|
2647
|
+
const resolution = readExistingResolution(resolutionPath, io, "recording_unresolved");
|
|
2648
|
+
closedObject(
|
|
2649
|
+
resolution,
|
|
2650
|
+
["schema", "assignment_id", "task_id", "resolvedAt", "completion_sha256", "steps"],
|
|
2651
|
+
"recording resolution",
|
|
2652
|
+
["resolvedAt"],
|
|
2653
|
+
"recording_unresolved",
|
|
2654
|
+
);
|
|
2655
|
+
if (resolution.schema !== RECORDING_RESOLUTION_SCHEMA) {
|
|
2656
|
+
throw failClosed("recording resolution schema is invalid", "recording_unresolved");
|
|
2657
|
+
}
|
|
2658
|
+
if (resolution.assignment_id !== completion.assignment_id || resolution.task_id !== completion.task_id) {
|
|
2659
|
+
throw failClosed("recording resolution is not bound to this assignment", "recording_unresolved");
|
|
2660
|
+
}
|
|
2661
|
+
const expectedSteps = {
|
|
2662
|
+
candidate_snapshot: original.candidate_snapshot,
|
|
2663
|
+
sidecars: recoverSidecarStep(original.sidecars),
|
|
2664
|
+
routing_record: "ok",
|
|
2665
|
+
telemetry: "ok",
|
|
2666
|
+
};
|
|
2667
|
+
try {
|
|
2668
|
+
assertResolutionMatches(resolution, expectedRecordingResolution(completion, bytes, expectedSteps));
|
|
2669
|
+
} catch (error) {
|
|
2670
|
+
throw failClosed(error?.message ?? "recording resolution does not match this completion", "recording_unresolved");
|
|
2671
|
+
}
|
|
2672
|
+
if (!recordingBookkeepingReady(resolution.steps)) {
|
|
2673
|
+
throw failClosed("recording resolution did not recover routing and telemetry", "recording_unresolved");
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
function witnessBindingCode(error) {
|
|
2678
|
+
const code = error?.runnerCode;
|
|
2679
|
+
if (code === "plan_ref_invalid" || code === "plan_mismatch") return "plan_mismatch";
|
|
2680
|
+
if (code === "recording_unresolved") return "recording_unresolved";
|
|
2681
|
+
if (WITNESS_CODES.includes(code)) return code;
|
|
2682
|
+
return "witness_binding_invalid";
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
function fallbackWitnessPlan(completion) {
|
|
2686
|
+
if (completion.plan?.kind === "bootstrap") {
|
|
2687
|
+
return Object.freeze({ kind: "bootstrap", reason: completion.plan.reason });
|
|
2688
|
+
}
|
|
2689
|
+
return Object.freeze({ kind: "current", sha256: completion.plan?.sha256 });
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
function writeEarlyWitnessRefusal(recordDir, io, identity, stored, loaded, plan, code, startedAt, startedMs, now) {
|
|
2693
|
+
const receiptId = nextWitnessReceiptId(witnessHistoryDir(recordDir), now, io.existsSync);
|
|
2694
|
+
const finishedAt = new Date(now()).toISOString();
|
|
2695
|
+
return writeWitnessReceipt(recordDir, {
|
|
2696
|
+
schema: WITNESS_SCHEMA,
|
|
2697
|
+
receipt_id: receiptId,
|
|
2698
|
+
task_id: identity.task_id,
|
|
2699
|
+
assignment_id: identity.assignment_id,
|
|
2700
|
+
kind: identity.kind,
|
|
2701
|
+
recordedAt: finishedAt,
|
|
2702
|
+
manifest: Object.freeze({ path: stored.path, sha256: stored.sha256 }),
|
|
2703
|
+
completion: Object.freeze({ path: loaded.path, sha256: sha256Bytes(loaded.bytes) }),
|
|
2704
|
+
plan: plan ?? fallbackWitnessPlan(loaded.completion),
|
|
2705
|
+
binding: Object.freeze({
|
|
2706
|
+
task_dir: identity.task_dir,
|
|
2707
|
+
cwd: identity.cwd,
|
|
2708
|
+
record_dir: identity.record_dir,
|
|
2709
|
+
}),
|
|
2710
|
+
candidate: Object.freeze({}),
|
|
2711
|
+
gates: [],
|
|
2712
|
+
result: "refused",
|
|
2713
|
+
code,
|
|
2714
|
+
startedAt,
|
|
2715
|
+
finishedAt,
|
|
2716
|
+
durationMs: Math.max(0, Date.now() - startedMs),
|
|
2717
|
+
}, {}, io);
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
function contractedWitnessId(manifest) {
|
|
2721
|
+
const id = manifest?.contract?.witness?.id;
|
|
2722
|
+
if (!WITNESS_IDS.includes(id)) {
|
|
2723
|
+
throw failClosed("stored contract.witness.id is not a fixed gate", "witness_binding_invalid");
|
|
2724
|
+
}
|
|
2725
|
+
if (WRITER_KINDS.includes(manifest.kind) && id !== VERIFY_WITNESS_ID) {
|
|
2726
|
+
throw failClosed("implement and repair require the fixed verify witness", "witness_binding_invalid");
|
|
2727
|
+
}
|
|
2728
|
+
return id;
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
function gatesForWitnessId(witnessId) {
|
|
2732
|
+
return Object.freeze(WITNESS_IDS.filter((id) => id === witnessId).map((id) => ({
|
|
2733
|
+
id,
|
|
2734
|
+
argv: [...FIXED_GATES[id]],
|
|
2735
|
+
})));
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
function runFixedGate(gate, cwd, spawnSyncImpl, env) {
|
|
2739
|
+
const argv = [...FIXED_GATES[gate.id]];
|
|
2740
|
+
const started = Date.now();
|
|
2741
|
+
const result = spawnSyncImpl(argv[0], argv.slice(1), {
|
|
2742
|
+
cwd,
|
|
2743
|
+
env,
|
|
2744
|
+
encoding: "utf8",
|
|
2745
|
+
shell: false,
|
|
2746
|
+
windowsHide: true,
|
|
2747
|
+
});
|
|
2748
|
+
const durationMs = Math.max(0, Date.now() - started);
|
|
2749
|
+
return {
|
|
2750
|
+
id: gate.id,
|
|
2751
|
+
argv,
|
|
2752
|
+
shell: false,
|
|
2753
|
+
cwd,
|
|
2754
|
+
exitCode: Number.isInteger(result?.status) ? result.status : result?.status === null ? null : null,
|
|
2755
|
+
signal: typeof result?.signal === "string" ? result.signal : null,
|
|
2756
|
+
timedOut: false,
|
|
2757
|
+
durationMs,
|
|
2758
|
+
stdout: String(result?.stdout ?? ""),
|
|
2759
|
+
stderr: String(result?.stderr ?? ""),
|
|
2760
|
+
spawnError: result?.error ? String(result.error.code ?? result.error.message ?? "spawn_failed") : undefined,
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
function gateSucceeded(gate) {
|
|
2765
|
+
return gate.exitCode === 0 && gate.signal == null && !gate.spawnError;
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
function ensureWitnessDirs(recordDir, io) {
|
|
2769
|
+
const root = witnessDir(recordDir);
|
|
2770
|
+
const history = witnessHistoryDir(recordDir);
|
|
2771
|
+
try {
|
|
2772
|
+
if (!io.existsSync(root)) io.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
2773
|
+
if (!io.existsSync(history)) io.mkdirSync(history, { recursive: true, mode: 0o700 });
|
|
2774
|
+
} catch (error) {
|
|
2775
|
+
throw failClosed(`cannot create witness directories: ${error?.message ?? error}`, "record_write_failed");
|
|
2776
|
+
}
|
|
2777
|
+
return { root, history };
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
function pickPublicWitness(receipt) {
|
|
2781
|
+
const picked = {};
|
|
2782
|
+
for (const key of WITNESS_RECEIPT_KEYS) {
|
|
2783
|
+
if (receipt[key] !== undefined) picked[key] = receipt[key];
|
|
2784
|
+
}
|
|
2785
|
+
return picked;
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
function writeWitnessLatest(recordDir, receipt, receiptPath, receiptSha, io) {
|
|
2789
|
+
const latest = {
|
|
2790
|
+
schema: WITNESS_LATEST_SCHEMA,
|
|
2791
|
+
task_id: receipt.task_id,
|
|
2792
|
+
assignment_id: receipt.assignment_id,
|
|
2793
|
+
receipt_id: receipt.receipt_id,
|
|
2794
|
+
path: receiptPath,
|
|
2795
|
+
sha256: receiptSha,
|
|
2796
|
+
recordedAt: receipt.recordedAt,
|
|
2797
|
+
result: receipt.result,
|
|
2798
|
+
};
|
|
2799
|
+
const body = `${JSON.stringify(latest, undefined, 2)}\n`;
|
|
2800
|
+
const path = witnessLatestPath(recordDir);
|
|
2801
|
+
writePrivate(path, body, io, "w");
|
|
2802
|
+
return sidecarRef(path, bytesOf(body));
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
function writeWitnessReceipt(recordDir, receipt, logs, io) {
|
|
2806
|
+
const dirs = ensureWitnessDirs(recordDir, io);
|
|
2807
|
+
const receiptId = receipt.receipt_id;
|
|
2808
|
+
const artifactDir = join(dirs.history, receiptId);
|
|
2809
|
+
try {
|
|
2810
|
+
io.mkdirSync(artifactDir, { recursive: true, mode: 0o700 });
|
|
2811
|
+
} catch (error) {
|
|
2812
|
+
throw failClosed(`cannot create witness receipt directory: ${error?.message ?? error}`, "record_write_failed");
|
|
2813
|
+
}
|
|
2814
|
+
const publicGates = [];
|
|
2815
|
+
for (const gate of receipt.gates) {
|
|
2816
|
+
const logName = `${gate.id}.log`;
|
|
2817
|
+
const logPath = join(artifactDir, logName);
|
|
2818
|
+
const logBody = logs[gate.id] ?? "";
|
|
2819
|
+
writePrivate(logPath, logBody, io, "wx");
|
|
2820
|
+
publicGates.push(Object.freeze({
|
|
2821
|
+
id: gate.id,
|
|
2822
|
+
argv: Object.freeze([...gate.argv]),
|
|
2823
|
+
shell: false,
|
|
2824
|
+
cwd: gate.cwd,
|
|
2825
|
+
exitCode: gate.exitCode,
|
|
2826
|
+
signal: gate.signal,
|
|
2827
|
+
timedOut: gate.timedOut === true,
|
|
2828
|
+
durationMs: gate.durationMs,
|
|
2829
|
+
log: sidecarRef(logPath, bytesOf(logBody)),
|
|
2830
|
+
}));
|
|
2831
|
+
}
|
|
2832
|
+
const publicReceipt = pickPublicWitness({
|
|
2833
|
+
...receipt,
|
|
2834
|
+
gates: Object.freeze(publicGates),
|
|
2835
|
+
});
|
|
2836
|
+
const receiptPath = join(dirs.history, `${receiptId}.json`);
|
|
2837
|
+
const body = `${JSON.stringify(publicReceipt, undefined, 2)}\n`;
|
|
2838
|
+
writePrivate(receiptPath, body, io, "wx");
|
|
2839
|
+
const sha = sha256Bytes(Buffer.from(body));
|
|
2840
|
+
writeWitnessLatest(recordDir, publicReceipt, receiptPath, sha, io);
|
|
2841
|
+
return Object.freeze({ ...publicReceipt, path: receiptPath, sha256: sha });
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
function isRecordedWitnessSnapshot(snapshot) {
|
|
2845
|
+
return Boolean(
|
|
2846
|
+
snapshot
|
|
2847
|
+
&& snapshot.status !== "unknown"
|
|
2848
|
+
&& typeof snapshot.head === "string"
|
|
2849
|
+
&& HEX40.test(snapshot.head)
|
|
2850
|
+
&& typeof snapshot.index_tree === "string"
|
|
2851
|
+
&& HEX40.test(snapshot.index_tree),
|
|
2852
|
+
);
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
function classifyWitnessResult({ before, after, gates, ranGates }) {
|
|
2856
|
+
if (!ranGates) {
|
|
2857
|
+
if (!isRecordedWitnessSnapshot(before)) return { result: "refused", code: "snapshot_failed" };
|
|
2858
|
+
if (before.clean !== true) return { result: "refused", code: "dirty_baseline" };
|
|
2859
|
+
return { result: "refused", code: "unknown" };
|
|
2860
|
+
}
|
|
2861
|
+
const gateFail = gates.some((gate) => !gateSucceeded(gate));
|
|
2862
|
+
if (!isRecordedWitnessSnapshot(after)) {
|
|
2863
|
+
return { result: "failed", code: gateFail ? "gate_failed" : "snapshot_failed" };
|
|
2864
|
+
}
|
|
2865
|
+
const unchanged = before.head === after.head
|
|
2866
|
+
&& before.index_tree === after.index_tree
|
|
2867
|
+
&& before.clean === true
|
|
2868
|
+
&& after.clean === true;
|
|
2869
|
+
if (gateFail) return { result: "failed", code: "gate_failed" };
|
|
2870
|
+
if (!unchanged) return { result: "failed", code: "candidate_changed" };
|
|
2871
|
+
return { result: "passed" };
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
export async function witnessAssignment(recordDirValue, deps = {}) {
|
|
2875
|
+
const io = ioDeps(deps);
|
|
2876
|
+
const startedMs = Date.now();
|
|
2877
|
+
const startedAt = new Date((deps.now ?? Date.now)()).toISOString();
|
|
2878
|
+
if (typeof recordDirValue !== "string" || !nodePath.isAbsolute(recordDirValue)) {
|
|
2879
|
+
throw failClosed("--record-dir must be an absolute path", "manifest_invalid");
|
|
2880
|
+
}
|
|
2881
|
+
if (!io.existsSync(recordDirValue)) {
|
|
2882
|
+
throw failClosed(`record directory does not exist: ${recordDirValue}`, "completion_missing");
|
|
2883
|
+
}
|
|
2884
|
+
const recordDir = comparablePath(recordDirValue, io.realpathSync, io.lstatSync);
|
|
2885
|
+
const loaded = loadWitnessCompletion(recordDir, io);
|
|
2886
|
+
const completion = loaded.completion;
|
|
2887
|
+
const stored = loadStoredManifest(recordDir, completion, io);
|
|
2888
|
+
const now = deps.now ?? Date.now;
|
|
2889
|
+
let identity;
|
|
2890
|
+
let plan;
|
|
2891
|
+
try {
|
|
2892
|
+
identity = validateStoredManifestBinding(stored.manifest, recordDir, io);
|
|
2893
|
+
assertCompletionMatchesStored(completion, identity, io);
|
|
2894
|
+
plan = bindWitnessPlan(completion, identity, io);
|
|
2895
|
+
assertWitnessRecordingReady(completion, loaded.bytes, recordDir, io);
|
|
2896
|
+
} catch (error) {
|
|
2897
|
+
if (!identity) {
|
|
2898
|
+
try {
|
|
2899
|
+
identity = bindWitnessIdentityFromCompletion(completion, recordDir, io);
|
|
2900
|
+
} catch {
|
|
2901
|
+
identity = Object.freeze({
|
|
2902
|
+
task_id: typeof stored.manifest.task_id === "string" ? stored.manifest.task_id : completion.task_id,
|
|
2903
|
+
assignment_id: typeof stored.manifest.assignment_id === "string" ? stored.manifest.assignment_id : completion.assignment_id,
|
|
2904
|
+
kind: typeof stored.manifest.kind === "string" ? stored.manifest.kind : completion.kind,
|
|
2905
|
+
task_dir: resolvePath(stored.manifest.task_dir ?? completion.binding?.task_dir ?? recordDir),
|
|
2906
|
+
cwd: resolvePath(stored.manifest.cwd ?? completion.binding?.cwd ?? recordDir),
|
|
2907
|
+
record_dir: recordDir,
|
|
2908
|
+
});
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
return writeEarlyWitnessRefusal(
|
|
2912
|
+
recordDir,
|
|
2913
|
+
io,
|
|
2914
|
+
identity,
|
|
2915
|
+
stored,
|
|
2916
|
+
loaded,
|
|
2917
|
+
plan,
|
|
2918
|
+
witnessBindingCode(error),
|
|
2919
|
+
startedAt,
|
|
2920
|
+
startedMs,
|
|
2921
|
+
now,
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
let witnessId;
|
|
2926
|
+
try {
|
|
2927
|
+
witnessId = contractedWitnessId(stored.manifest);
|
|
2928
|
+
} catch (error) {
|
|
2929
|
+
return writeEarlyWitnessRefusal(
|
|
2930
|
+
recordDir,
|
|
2931
|
+
io,
|
|
2932
|
+
identity,
|
|
2933
|
+
stored,
|
|
2934
|
+
loaded,
|
|
2935
|
+
plan,
|
|
2936
|
+
witnessCode(error?.runnerCode),
|
|
2937
|
+
startedAt,
|
|
2938
|
+
startedMs,
|
|
2939
|
+
now,
|
|
2940
|
+
);
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
const before = snapshotWitnessCandidate(identity.cwd, io.spawnSync, io.realpathSync);
|
|
2944
|
+
const candidate = { before: recordedSnapshot(before) };
|
|
2945
|
+
const dirtyOrUnknown = before.status !== "recorded" || before.clean !== true;
|
|
2946
|
+
const logs = {};
|
|
2947
|
+
let gates = [];
|
|
2948
|
+
let ranGates = false;
|
|
2949
|
+
if (!dirtyOrUnknown) {
|
|
2950
|
+
ranGates = true;
|
|
2951
|
+
const env = deps.env ?? process.env;
|
|
2952
|
+
gates = gatesForWitnessId(witnessId).map((gate) => {
|
|
2953
|
+
const executed = runFixedGate(gate, identity.cwd, io.spawnSync, env);
|
|
2954
|
+
logs[executed.id] = [
|
|
2955
|
+
`argv=${JSON.stringify(executed.argv)}`,
|
|
2956
|
+
`shell=false`,
|
|
2957
|
+
`cwd=${executed.cwd}`,
|
|
2958
|
+
`exitCode=${executed.exitCode}`,
|
|
2959
|
+
`signal=${executed.signal ?? ""}`,
|
|
2960
|
+
executed.spawnError ? `spawnError=${executed.spawnError}` : "",
|
|
2961
|
+
executed.stdout ? `stdout:\n${executed.stdout}` : "",
|
|
2962
|
+
executed.stderr ? `stderr:\n${executed.stderr}` : "",
|
|
2963
|
+
].filter((line) => line !== "").join("\n");
|
|
2964
|
+
return executed;
|
|
2965
|
+
});
|
|
2966
|
+
candidate.after = recordedSnapshot(snapshotWitnessCandidate(identity.cwd, io.spawnSync, io.realpathSync));
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
const classified = classifyWitnessResult({
|
|
2970
|
+
before,
|
|
2971
|
+
after: candidate.after,
|
|
2972
|
+
gates,
|
|
2973
|
+
ranGates,
|
|
2974
|
+
});
|
|
2975
|
+
const receiptId = nextWitnessReceiptId(witnessHistoryDir(recordDir), deps.now ?? Date.now, io.existsSync);
|
|
2976
|
+
const finishedAt = new Date((deps.now ?? Date.now)()).toISOString();
|
|
2977
|
+
const receipt = {
|
|
2978
|
+
schema: WITNESS_SCHEMA,
|
|
2979
|
+
receipt_id: receiptId,
|
|
2980
|
+
task_id: identity.task_id,
|
|
2981
|
+
assignment_id: identity.assignment_id,
|
|
2982
|
+
kind: identity.kind,
|
|
2983
|
+
recordedAt: finishedAt,
|
|
2984
|
+
manifest: Object.freeze({ path: stored.path, sha256: stored.sha256 }),
|
|
2985
|
+
completion: Object.freeze({ path: loaded.path, sha256: sha256Bytes(loaded.bytes) }),
|
|
2986
|
+
plan,
|
|
2987
|
+
binding: Object.freeze({
|
|
2988
|
+
task_dir: identity.task_dir,
|
|
2989
|
+
cwd: identity.cwd,
|
|
2990
|
+
record_dir: identity.record_dir,
|
|
2991
|
+
}),
|
|
2992
|
+
candidate: Object.freeze(candidate),
|
|
2993
|
+
gates,
|
|
2994
|
+
result: classified.result,
|
|
2995
|
+
startedAt,
|
|
2996
|
+
finishedAt,
|
|
2997
|
+
durationMs: Math.max(0, Date.now() - startedMs),
|
|
2998
|
+
};
|
|
2999
|
+
if (classified.code) receipt.code = classified.code;
|
|
3000
|
+
return writeWitnessReceipt(recordDir, receipt, logs, io);
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
const ACCEPT_SINGLETONS = Object.freeze([
|
|
3004
|
+
"--task-dir",
|
|
3005
|
+
"--commit",
|
|
3006
|
+
"--record-dir",
|
|
3007
|
+
"--observed-pr",
|
|
3008
|
+
"--observed-ci",
|
|
3009
|
+
]);
|
|
3010
|
+
const ACCEPTED_KEYS = Object.freeze([
|
|
3011
|
+
"schema",
|
|
3012
|
+
"task_id",
|
|
3013
|
+
"commit",
|
|
3014
|
+
"tree",
|
|
3015
|
+
"writer",
|
|
3016
|
+
"plan",
|
|
3017
|
+
"critics",
|
|
3018
|
+
"acceptedAt",
|
|
3019
|
+
"observed",
|
|
3020
|
+
"note",
|
|
3021
|
+
]);
|
|
3022
|
+
const ACCEPTED_WRITER_KEYS = Object.freeze([
|
|
3023
|
+
"assignment_id",
|
|
3024
|
+
"record_dir",
|
|
3025
|
+
"kind",
|
|
3026
|
+
"completion",
|
|
3027
|
+
"manifest",
|
|
3028
|
+
"receipt",
|
|
3029
|
+
]);
|
|
3030
|
+
const ACCEPTED_CRITIC_KEYS = Object.freeze([
|
|
3031
|
+
"kind",
|
|
3032
|
+
"role",
|
|
3033
|
+
"harness",
|
|
3034
|
+
"model",
|
|
3035
|
+
"assignment_id",
|
|
3036
|
+
"record_dir",
|
|
3037
|
+
"completion",
|
|
3038
|
+
"verdict",
|
|
3039
|
+
"judged_tree",
|
|
3040
|
+
]);
|
|
3041
|
+
const ACCEPTED_LINK_KEYS = Object.freeze(["path", "sha256"]);
|
|
3042
|
+
const ACCEPTED_RECEIPT_KEYS = Object.freeze(["id", "path", "sha256"]);
|
|
3043
|
+
const OBSERVED_ID_KEYS = Object.freeze(["id", "validated"]);
|
|
3044
|
+
const TASK_DIR_SKIP_NAMES = Object.freeze([
|
|
3045
|
+
PLAN_POINTER_FILENAME,
|
|
3046
|
+
ACCEPTED_FILENAME,
|
|
3047
|
+
"runner-errors.jsonl",
|
|
3048
|
+
"plan-history",
|
|
3049
|
+
]);
|
|
3050
|
+
|
|
3051
|
+
function parseAcceptCli(args) {
|
|
3052
|
+
if (args[0] !== "accept") {
|
|
3053
|
+
throw failClosed(CLI_USAGE, "manifest_invalid");
|
|
3054
|
+
}
|
|
3055
|
+
const rest = args.slice(1);
|
|
3056
|
+
const seen = new Set();
|
|
3057
|
+
const critics = [];
|
|
3058
|
+
const values = {};
|
|
3059
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
3060
|
+
const flag = rest[index];
|
|
3061
|
+
if (typeof flag !== "string" || !flag.startsWith("--")) {
|
|
3062
|
+
throw failClosed(CLI_USAGE, "manifest_invalid");
|
|
3063
|
+
}
|
|
3064
|
+
if (flag !== "--critic" && !ACCEPT_SINGLETONS.includes(flag)) {
|
|
3065
|
+
throw failClosed(`unknown accept option ${flag}`, "manifest_invalid");
|
|
3066
|
+
}
|
|
3067
|
+
const value = rest[index + 1];
|
|
3068
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
3069
|
+
throw failClosed(`missing value for ${flag}`, "manifest_invalid");
|
|
3070
|
+
}
|
|
3071
|
+
index += 1;
|
|
3072
|
+
if (flag === "--critic") {
|
|
3073
|
+
critics.push(value);
|
|
3074
|
+
continue;
|
|
3075
|
+
}
|
|
3076
|
+
if (seen.has(flag)) {
|
|
3077
|
+
throw failClosed(`duplicate option ${flag}`, "manifest_invalid");
|
|
3078
|
+
}
|
|
3079
|
+
seen.add(flag);
|
|
3080
|
+
values[flag] = value;
|
|
3081
|
+
}
|
|
3082
|
+
if (values["--task-dir"] === undefined || values["--commit"] === undefined || values["--record-dir"] === undefined) {
|
|
3083
|
+
throw failClosed(CLI_USAGE, "manifest_invalid");
|
|
3084
|
+
}
|
|
3085
|
+
if (critics.length !== 2) {
|
|
3086
|
+
throw failClosed("accept requires exactly two --critic record directories", "manifest_invalid");
|
|
3087
|
+
}
|
|
3088
|
+
return Object.freeze({
|
|
3089
|
+
taskDir: values["--task-dir"],
|
|
3090
|
+
commit: values["--commit"],
|
|
3091
|
+
recordDir: values["--record-dir"],
|
|
3092
|
+
critics: Object.freeze([...critics]),
|
|
3093
|
+
observedPr: values["--observed-pr"],
|
|
3094
|
+
observedCi: values["--observed-ci"],
|
|
3095
|
+
});
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
function requireAbsoluteExistingDir(value, label, code, io) {
|
|
3099
|
+
const path = requireNonemptyString(value, label, code);
|
|
3100
|
+
if (!nodePath.isAbsolute(path)) {
|
|
3101
|
+
throw failClosed(`${label} must be an absolute path`, code);
|
|
3102
|
+
}
|
|
3103
|
+
if (!io.existsSync(path)) {
|
|
3104
|
+
throw failClosed(`${label} does not exist: ${path}`, code);
|
|
3105
|
+
}
|
|
3106
|
+
let stat;
|
|
3107
|
+
try {
|
|
3108
|
+
stat = io.statSync(path);
|
|
3109
|
+
} catch {
|
|
3110
|
+
throw failClosed(`${label} is not a directory: ${path}`, code);
|
|
3111
|
+
}
|
|
3112
|
+
if (!stat.isDirectory()) {
|
|
3113
|
+
throw failClosed(`${label} is not a directory: ${path}`, code);
|
|
3114
|
+
}
|
|
3115
|
+
return comparablePath(path, io.realpathSync, io.lstatSync);
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
function assertCanonicalRecordDir(taskDir, recordDirValue, label, io) {
|
|
3119
|
+
const path = requireNonemptyString(recordDirValue, label, "manifest_invalid");
|
|
3120
|
+
if (!nodePath.isAbsolute(path)) {
|
|
3121
|
+
throw failClosed(`${label} must be an absolute path`, "manifest_invalid");
|
|
3122
|
+
}
|
|
3123
|
+
if (!io.existsSync(path)) {
|
|
3124
|
+
throw failClosed(`${label} does not exist: ${path}`, "completion_missing");
|
|
3125
|
+
}
|
|
3126
|
+
let stat;
|
|
3127
|
+
try {
|
|
3128
|
+
stat = io.lstatSync(path);
|
|
3129
|
+
} catch {
|
|
3130
|
+
throw failClosed(`${label} is not a directory: ${path}`, "record_outside_task");
|
|
3131
|
+
}
|
|
3132
|
+
if (!stat.isDirectory()) {
|
|
3133
|
+
throw failClosed(`${label} is not a directory: ${path}`, "record_outside_task");
|
|
3134
|
+
}
|
|
3135
|
+
const recordDir = comparablePath(path, io.realpathSync, io.lstatSync);
|
|
3136
|
+
if (dirname(recordDir) !== taskDir) {
|
|
3137
|
+
throw failClosed(`${label} must be a canonical direct child of task_dir`, "record_outside_task");
|
|
3138
|
+
}
|
|
3139
|
+
const assignmentId = basename(recordDir);
|
|
3140
|
+
if (!IDENTITY.test(assignmentId)) {
|
|
3141
|
+
throw failClosed(`${label} directory name must be a stable identity token`, "record_outside_task");
|
|
3142
|
+
}
|
|
3143
|
+
return recordDir;
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
function loadStructurallyBoundAssignment(recordDir, expectedTaskDir, io) {
|
|
3147
|
+
const loaded = loadWitnessCompletion(recordDir, io);
|
|
3148
|
+
const stored = loadStoredManifest(recordDir, loaded.completion, io);
|
|
3149
|
+
let identity;
|
|
3150
|
+
try {
|
|
3151
|
+
identity = validateStoredManifestBinding(stored.manifest, recordDir, io);
|
|
3152
|
+
if (!samePath(identity.task_dir, expectedTaskDir, io.realpathSync)) {
|
|
3153
|
+
throw failClosed("stored assignment task_dir does not match --task-dir", "witness_binding_invalid");
|
|
3154
|
+
}
|
|
3155
|
+
assertCompletionMatchesStored(loaded.completion, identity, io);
|
|
3156
|
+
const plan = bindWitnessPlan(loaded.completion, identity, io);
|
|
3157
|
+
return { loaded, stored, identity, plan, completion: loaded.completion };
|
|
3158
|
+
} catch (error) {
|
|
3159
|
+
if (error?.runnerCode === "plan_ref_invalid") {
|
|
3160
|
+
throw failClosed(error.message, "plan_mismatch");
|
|
3161
|
+
}
|
|
3162
|
+
throw error;
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
function loadBoundAssignment(recordDir, expectedTaskDir, io) {
|
|
3167
|
+
const bound = loadStructurallyBoundAssignment(recordDir, expectedTaskDir, io);
|
|
3168
|
+
assertWitnessRecordingReady(bound.completion, bound.loaded.bytes, recordDir, io);
|
|
3169
|
+
return bound;
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
function listHistoryReceiptIds(recordDir, io) {
|
|
3173
|
+
const historyDir = witnessHistoryDir(recordDir);
|
|
3174
|
+
if (!io.existsSync(historyDir)) return [];
|
|
3175
|
+
let names;
|
|
3176
|
+
try {
|
|
3177
|
+
names = io.readdirSync(historyDir);
|
|
3178
|
+
} catch (error) {
|
|
3179
|
+
throw failClosed(`cannot read witness history: ${error?.message ?? error}`, "witness_missing");
|
|
3180
|
+
}
|
|
3181
|
+
const ids = [];
|
|
3182
|
+
for (const name of names) {
|
|
3183
|
+
const entry = typeof name === "string" ? name : name?.name;
|
|
3184
|
+
if (typeof entry !== "string" || !entry.endsWith(".json")) continue;
|
|
3185
|
+
ids.push(entry.slice(0, -5));
|
|
3186
|
+
}
|
|
3187
|
+
return ids;
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
function loadLatestPassedWitness(recordDir, identity, stored, io) {
|
|
3191
|
+
const latestPath = witnessLatestPath(recordDir);
|
|
3192
|
+
if (!io.existsSync(latestPath)) {
|
|
3193
|
+
throw failClosed("missing latest witness pointer", "witness_missing");
|
|
3194
|
+
}
|
|
3195
|
+
const latestBytes = readRegularFile(latestPath, io.readFileSync, io.existsSync, io.statSync, "witness_missing");
|
|
3196
|
+
const latest = parseJsonFile(latestPath, latestBytes, "latest witness pointer", "witness_stale");
|
|
3197
|
+
closedObject(latest, WITNESS_LATEST_KEYS, "latest witness pointer", [], "witness_stale");
|
|
3198
|
+
if (latest.schema !== WITNESS_LATEST_SCHEMA) {
|
|
3199
|
+
throw failClosed(`latest witness schema must be ${WITNESS_LATEST_SCHEMA}`, "witness_stale");
|
|
3200
|
+
}
|
|
3201
|
+
if (latest.task_id !== identity.task_id || latest.assignment_id !== identity.assignment_id) {
|
|
3202
|
+
throw failClosed("latest witness pointer is not bound to this assignment", "witness_stale");
|
|
3203
|
+
}
|
|
3204
|
+
const receiptPath = requireNonemptyString(latest.path, "latest.path", "witness_stale");
|
|
3205
|
+
if (!nodePath.isAbsolute(receiptPath)) {
|
|
3206
|
+
throw failClosed("latest witness path must be absolute", "witness_stale");
|
|
3207
|
+
}
|
|
3208
|
+
const historyDir = comparablePath(witnessHistoryDir(recordDir), io.realpathSync, io.lstatSync);
|
|
3209
|
+
const receiptDir = comparablePath(dirname(receiptPath), io.realpathSync, io.lstatSync);
|
|
3210
|
+
if (receiptDir !== historyDir) {
|
|
3211
|
+
throw failClosed("latest witness receipt is not in this assignment history", "witness_stale");
|
|
3212
|
+
}
|
|
3213
|
+
const receiptBytes = readRegularFile(receiptPath, io.readFileSync, io.existsSync, io.statSync, "witness_missing");
|
|
3214
|
+
const actualSha = sha256Bytes(receiptBytes);
|
|
3215
|
+
if (actualSha !== requireHex(latest.sha256, "latest.sha256", HEX64, "witness_stale")) {
|
|
3216
|
+
throw failClosed("latest witness receipt digest does not match", "witness_stale");
|
|
3217
|
+
}
|
|
3218
|
+
const receipt = parseJsonFile(receiptPath, receiptBytes, "witness receipt", "witness_stale");
|
|
3219
|
+
if (!isPlainObject(receipt) || receipt.schema !== WITNESS_SCHEMA) {
|
|
3220
|
+
throw failClosed(`witness receipt schema must be ${WITNESS_SCHEMA}`, "witness_stale");
|
|
3221
|
+
}
|
|
3222
|
+
if (receipt.receipt_id !== latest.receipt_id) {
|
|
3223
|
+
throw failClosed("latest witness pointer does not match receipt_id", "witness_stale");
|
|
3224
|
+
}
|
|
3225
|
+
for (const id of listHistoryReceiptIds(recordDir, io)) {
|
|
3226
|
+
if (id > latest.receipt_id) {
|
|
3227
|
+
throw failClosed("latest witness pointer is stale", "witness_stale");
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
if (receipt.result !== "passed") {
|
|
3231
|
+
throw failClosed("latest witness receipt is not passed", "witness_failed");
|
|
3232
|
+
}
|
|
3233
|
+
if (receipt.task_id !== identity.task_id || receipt.assignment_id !== identity.assignment_id) {
|
|
3234
|
+
throw failClosed("witness receipt identity does not match stored assignment", "witness_binding_invalid");
|
|
3235
|
+
}
|
|
3236
|
+
if (receipt.kind !== identity.kind) {
|
|
3237
|
+
throw failClosed("witness receipt kind does not match stored assignment", "witness_binding_invalid");
|
|
3238
|
+
}
|
|
3239
|
+
if (typeof receipt.manifest?.sha256 !== "string" || receipt.manifest.sha256 !== stored.sha256) {
|
|
3240
|
+
throw failClosed("witness receipt manifest digest does not match stored manifest", "witness_binding_invalid");
|
|
3241
|
+
}
|
|
3242
|
+
return { latest, receipt, path: receiptPath, sha256: actualSha };
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
function assertReceiptMatchesWriter(receipt, identity, stored, loaded, plan, io) {
|
|
3246
|
+
if (typeof receipt.manifest?.sha256 !== "string" || receipt.manifest.sha256 !== stored.sha256) {
|
|
3247
|
+
throw failClosed("witness receipt manifest digest does not match stored manifest", "witness_binding_invalid");
|
|
3248
|
+
}
|
|
3249
|
+
if (typeof receipt.completion?.sha256 !== "string" || receipt.completion.sha256 !== sha256Bytes(loaded.bytes)) {
|
|
3250
|
+
throw failClosed("witness receipt completion digest does not match", "witness_binding_invalid");
|
|
3251
|
+
}
|
|
3252
|
+
if (!samePath(receipt.binding?.task_dir, identity.task_dir, io.realpathSync)) {
|
|
3253
|
+
throw failClosed("witness receipt task_dir does not match stored assignment", "witness_binding_invalid");
|
|
3254
|
+
}
|
|
3255
|
+
if (!samePath(receipt.binding?.cwd, identity.cwd, io.realpathSync)) {
|
|
3256
|
+
throw failClosed("witness receipt cwd does not match stored assignment", "witness_binding_invalid");
|
|
3257
|
+
}
|
|
3258
|
+
if (!samePath(receipt.binding?.record_dir, identity.record_dir, io.realpathSync)) {
|
|
3259
|
+
throw failClosed("witness receipt record_dir does not match stored assignment", "witness_binding_invalid");
|
|
3260
|
+
}
|
|
3261
|
+
if (plan.kind === "current") {
|
|
3262
|
+
if (receipt.plan?.kind !== "current") {
|
|
3263
|
+
throw failClosed("witness receipt plan is not current", "plan_mismatch");
|
|
3264
|
+
}
|
|
3265
|
+
if (receipt.plan.sha256 !== plan.sha256) {
|
|
3266
|
+
throw failClosed("witness receipt plan hash does not match current plan", "plan_mismatch");
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
if (!Array.isArray(receipt.gates) || receipt.gates.length < 1) {
|
|
3270
|
+
throw failClosed("passed witness receipt is missing the contracted verify gate", "witness_failed");
|
|
3271
|
+
}
|
|
3272
|
+
const verify = receipt.gates.find((gate) => gate.id === VERIFY_WITNESS_ID);
|
|
3273
|
+
if (!verify) {
|
|
3274
|
+
throw failClosed("passed witness receipt is missing the contracted verify gate", "witness_failed");
|
|
3275
|
+
}
|
|
3276
|
+
if (!Array.isArray(verify.argv) || verify.argv.length !== 3
|
|
3277
|
+
|| verify.argv[0] !== "npm" || verify.argv[1] !== "run" || verify.argv[2] !== "verify") {
|
|
3278
|
+
throw failClosed("witness verify argv is not the fixed gate", "witness_failed");
|
|
3279
|
+
}
|
|
3280
|
+
if (verify.shell !== false) {
|
|
3281
|
+
throw failClosed("witness gates must execute shell:false", "witness_failed");
|
|
3282
|
+
}
|
|
3283
|
+
for (const gate of receipt.gates) {
|
|
3284
|
+
if (!samePath(gate.cwd, identity.cwd, io.realpathSync)) {
|
|
3285
|
+
throw failClosed("witness gate cwd does not match stored assignment cwd", "witness_binding_invalid");
|
|
3286
|
+
}
|
|
3287
|
+
if (gate.exitCode !== 0 || gate.signal != null || gate.timedOut === true) {
|
|
3288
|
+
throw failClosed("witness gate exits are not all successful", "witness_failed");
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
const before = receipt.candidate?.before;
|
|
3292
|
+
const after = receipt.candidate?.after;
|
|
3293
|
+
if (!isRecordedWitnessSnapshot(before) || !isRecordedWitnessSnapshot(after)) {
|
|
3294
|
+
throw failClosed("passed witness receipt is missing recorded snapshots", "witness_failed");
|
|
3295
|
+
}
|
|
3296
|
+
if (before.clean !== true || after.clean !== true
|
|
3297
|
+
|| before.head !== after.head
|
|
3298
|
+
|| before.index_tree !== after.index_tree) {
|
|
3299
|
+
throw failClosed("witness before/after snapshots are not identical and clean", "witness_failed");
|
|
3300
|
+
}
|
|
3301
|
+
return before.index_tree;
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
function assertCurrentPlanAgreement(identity, completion, receipt, io) {
|
|
3305
|
+
if (identity.plan_ref.kind !== "current") {
|
|
3306
|
+
throw failClosed("acceptance requires a current plan pointer", "plan_mismatch");
|
|
3307
|
+
}
|
|
3308
|
+
const pointerPath = pointerPathFor(identity.task_dir);
|
|
3309
|
+
const pointerBytes = readRegularFile(pointerPath, io.readFileSync, io.existsSync, io.statSync, "plan_mismatch");
|
|
3310
|
+
const pointer = validatePointer(parseJsonFile(pointerPath, pointerBytes, "plan pointer", "plan_mismatch"));
|
|
3311
|
+
if (pointer.task_id !== identity.task_id) {
|
|
3312
|
+
throw failClosed("current plan pointer task_id does not match assignment", "plan_mismatch");
|
|
3313
|
+
}
|
|
3314
|
+
const planPath = resolvePath(identity.task_dir, pointer.plan_path);
|
|
3315
|
+
if (!samePath(planPath, identity.plan_ref.path, io.realpathSync)) {
|
|
3316
|
+
throw failClosed("current plan path does not match stored plan_ref", "plan_mismatch");
|
|
3317
|
+
}
|
|
3318
|
+
const planBytes = readRegularFile(planPath, io.readFileSync, io.existsSync, io.statSync, "plan_mismatch");
|
|
3319
|
+
const actual = sha256Bytes(planBytes);
|
|
3320
|
+
if (pointer.plan_sha256 !== actual || identity.plan_ref.sha256 !== actual) {
|
|
3321
|
+
throw failClosed("current plan pointer hash does not match plan file digest", "plan_mismatch");
|
|
3322
|
+
}
|
|
3323
|
+
if (completion.plan?.kind !== "current" || completion.plan.sha256 !== actual) {
|
|
3324
|
+
throw failClosed("completion plan hash does not match current plan", "plan_mismatch");
|
|
3325
|
+
}
|
|
3326
|
+
if (receipt.plan?.kind !== "current" || receipt.plan.sha256 !== actual) {
|
|
3327
|
+
throw failClosed("witness receipt plan hash does not match current plan", "plan_mismatch");
|
|
3328
|
+
}
|
|
3329
|
+
if (pointer.generation !== identity.plan_ref.generation) {
|
|
3330
|
+
throw failClosed("current plan generation does not match stored plan_ref", "plan_mismatch");
|
|
3331
|
+
}
|
|
3332
|
+
return Object.freeze({
|
|
3333
|
+
kind: "current",
|
|
3334
|
+
path: identity.plan_ref.path,
|
|
3335
|
+
sha256: actual,
|
|
3336
|
+
pointer_path: pointerPath,
|
|
3337
|
+
generation: pointer.generation,
|
|
3338
|
+
});
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
function proveAcceptedCommit(cwd, commitValue, spawnSyncImpl) {
|
|
3342
|
+
const commit = requireNonemptyString(commitValue, "--commit", "commit_missing");
|
|
3343
|
+
const typeRun = runGit(cwd, ["cat-file", "-t", commit], spawnSyncImpl);
|
|
3344
|
+
if (typeRun.result.status !== 0 || typeRun.stdout.trim() !== "commit") {
|
|
3345
|
+
throw failClosed(`--commit is not an existing commit object`, "commit_missing");
|
|
3346
|
+
}
|
|
3347
|
+
const resolvedRun = runGit(cwd, ["rev-parse", commit], spawnSyncImpl);
|
|
3348
|
+
const resolved = resolvedRun.stdout.trim();
|
|
3349
|
+
if (resolvedRun.result.status !== 0 || !HEX40.test(resolved)) {
|
|
3350
|
+
throw failClosed(`--commit is not an existing commit object`, "commit_missing");
|
|
3351
|
+
}
|
|
3352
|
+
const treeRun = runGit(cwd, ["rev-parse", `${commit}^{tree}`], spawnSyncImpl);
|
|
3353
|
+
const tree = treeRun.stdout.trim();
|
|
3354
|
+
if (treeRun.result.status !== 0 || !HEX40.test(tree)) {
|
|
3355
|
+
throw failClosed(`cannot derive tree for commit ${commit}`, "commit_missing");
|
|
3356
|
+
}
|
|
3357
|
+
return Object.freeze({ commit: resolved, tree });
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
function observedId(value, label) {
|
|
3361
|
+
if (value === undefined) return undefined;
|
|
3362
|
+
const id = requireNonemptyString(value, label, "manifest_invalid");
|
|
3363
|
+
return Object.freeze({ id, validated: false });
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
function closedObserved(pr, ci) {
|
|
3367
|
+
const observed = {};
|
|
3368
|
+
if (pr) observed.pr = pr;
|
|
3369
|
+
if (ci) observed.ci = ci;
|
|
3370
|
+
if (observed.pr) closedObject(observed.pr, OBSERVED_ID_KEYS, "observed.pr", [], "manifest_invalid");
|
|
3371
|
+
if (observed.ci) closedObject(observed.ci, OBSERVED_ID_KEYS, "observed.ci", [], "manifest_invalid");
|
|
3372
|
+
return Object.freeze(observed);
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
export function resolveRequiredCritics(policy) {
|
|
3376
|
+
if (!policy || typeof policy !== "object" || !policy.required_critics || !policy.routes) {
|
|
3377
|
+
return REQUIRED_ACCEPT_CRITICS;
|
|
3378
|
+
}
|
|
3379
|
+
const result = {};
|
|
3380
|
+
for (const [kind, routeId] of Object.entries(policy.required_critics)) {
|
|
3381
|
+
const route = policy.routes[routeId];
|
|
3382
|
+
if (!route || route.status !== "admitted") {
|
|
3383
|
+
throw failClosed(`required critic route ${routeId} is not admitted`, "critic_invalid");
|
|
3384
|
+
}
|
|
3385
|
+
const role = KIND_ROLES[kind];
|
|
3386
|
+
if (!role || !Array.isArray(route.roles) || !route.roles.includes(role)) {
|
|
3387
|
+
throw failClosed(`required critic route ${routeId} does not admit role ${role}`, "critic_invalid");
|
|
3388
|
+
}
|
|
3389
|
+
result[kind] = Object.freeze({
|
|
3390
|
+
harness: route.harness,
|
|
3391
|
+
model: route.model,
|
|
3392
|
+
role,
|
|
3393
|
+
vendor: canonicalVendor(route.vendor),
|
|
3394
|
+
route_id: routeId,
|
|
3395
|
+
});
|
|
3396
|
+
}
|
|
3397
|
+
const criticVendors = Object.values(result).map((spec) => spec.vendor);
|
|
3398
|
+
if (new Set(criticVendors).size !== criticVendors.length) {
|
|
3399
|
+
throw failClosed("critics must have independent vendors", "critic_invalid");
|
|
3400
|
+
}
|
|
3401
|
+
return Object.freeze(result);
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
function assertEligibleWriter(bound, policy) {
|
|
3405
|
+
if (!WRITER_KINDS.includes(bound.identity.kind)) {
|
|
3406
|
+
throw failClosed("acceptance writer must be implement or repair", "witness_binding_invalid");
|
|
3407
|
+
}
|
|
3408
|
+
const transport = bound.completion.transport;
|
|
3409
|
+
if (!(transport?.status === "completed" && transport?.ok === true)) {
|
|
3410
|
+
throw failClosed("writer transport is not completed/ok", "witness_binding_invalid");
|
|
3411
|
+
}
|
|
3412
|
+
if (policy?.lineup?.writer && policy?.routes) {
|
|
3413
|
+
const matched = findLineupRoute(policy, "writer", bound.identity.harness, bound.identity.model);
|
|
3414
|
+
if (!matched) {
|
|
3415
|
+
throw failClosed(`writer ${bound.identity.harness}/${bound.identity.model} is not admitted in lineup`, "witness_binding_invalid");
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3420
|
+
function requiredCriticSpec(kind, criticSpecs = REQUIRED_ACCEPT_CRITICS) {
|
|
3421
|
+
const spec = criticSpecs[kind];
|
|
3422
|
+
if (!spec) {
|
|
3423
|
+
throw failClosed(`unsupported critic kind ${kind}`, "critic_invalid");
|
|
3424
|
+
}
|
|
3425
|
+
return spec;
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
function declaredReviewedTree(identity, spawnSyncImpl) {
|
|
3429
|
+
const base = identity.base;
|
|
3430
|
+
if (!base || !HEX40.test(base.commit)) {
|
|
3431
|
+
throw failClosed("stored critic base.commit is missing", "critic_invalid");
|
|
3432
|
+
}
|
|
3433
|
+
if (base.kind === "staged") {
|
|
3434
|
+
return requireHex(base.index_tree, "stored base.index_tree", HEX40, "critic_tree_mismatch");
|
|
3435
|
+
}
|
|
3436
|
+
const treeRun = runGit(identity.cwd, ["rev-parse", `${base.commit}^{tree}`], spawnSyncImpl);
|
|
3437
|
+
const tree = treeRun.stdout.trim();
|
|
3438
|
+
if (treeRun.result.status !== 0 || !HEX40.test(tree)) {
|
|
3439
|
+
throw failClosed("cannot derive stored critic baseline tree", "critic_tree_mismatch");
|
|
3440
|
+
}
|
|
3441
|
+
return tree;
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
function assertEligibleCritic(bound, acceptedTree, io, writer, criticSpecs = REQUIRED_ACCEPT_CRITICS) {
|
|
3445
|
+
const kind = bound.identity.kind;
|
|
3446
|
+
const spec = requiredCriticSpec(kind, criticSpecs);
|
|
3447
|
+
if (bound.identity.harness !== spec.harness || bound.identity.model !== spec.model || bound.identity.role !== spec.role) {
|
|
3448
|
+
throw failClosed(`critic ${kind} must use ${spec.harness}/${spec.model}`, "critic_invalid");
|
|
3449
|
+
}
|
|
3450
|
+
if (!samePath(bound.identity.task_dir, writer.identity.task_dir, io.realpathSync)) {
|
|
3451
|
+
throw failClosed("critic task_dir does not match writer task_dir", "critic_invalid");
|
|
3452
|
+
}
|
|
3453
|
+
const transport = bound.completion.transport;
|
|
3454
|
+
if (!(transport?.status === "completed" && transport?.ok === true)) {
|
|
3455
|
+
throw failClosed("critic transport is not completed/ok", "critic_invalid");
|
|
3456
|
+
}
|
|
3457
|
+
const candidate = bound.completion.candidate;
|
|
3458
|
+
if (!candidate || candidate.status !== "recorded") {
|
|
3459
|
+
throw failClosed("critic candidate is unknown", "critic_invalid");
|
|
3460
|
+
}
|
|
3461
|
+
if (candidate.clean !== true) {
|
|
3462
|
+
throw failClosed("critic candidate is not a clean recorded snapshot", "critic_invalid");
|
|
3463
|
+
}
|
|
3464
|
+
if (candidate.head !== bound.identity.base.commit) {
|
|
3465
|
+
throw failClosed("critic candidate head does not match stored manifest base.commit", "critic_tree_mismatch");
|
|
3466
|
+
}
|
|
3467
|
+
const reviewedTree = declaredReviewedTree(bound.identity, io.spawnSync);
|
|
3468
|
+
if (candidate.index_tree !== reviewedTree) {
|
|
3469
|
+
throw failClosed("critic candidate tree does not match stored reviewed baseline", "critic_tree_mismatch");
|
|
3470
|
+
}
|
|
3471
|
+
if (candidate.index_tree !== acceptedTree) {
|
|
3472
|
+
throw failClosed("critic candidate tree does not match accepted tree", "critic_tree_mismatch");
|
|
3473
|
+
}
|
|
3474
|
+
const critic = bound.completion.critic;
|
|
3475
|
+
if (!isPlainObject(critic) || critic.kind !== "review") {
|
|
3476
|
+
throw failClosed("model_claim PASS is not a critic", "critic_invalid");
|
|
3477
|
+
}
|
|
3478
|
+
if (!REVIEW_VERDICTS.includes(critic.verdict)) {
|
|
3479
|
+
throw failClosed("critic has no validated verdict", "critic_invalid");
|
|
3480
|
+
}
|
|
3481
|
+
if (critic.verdict === "BLOCK") {
|
|
3482
|
+
throw failClosed("critic verdict is BLOCK", "critic_block");
|
|
3483
|
+
}
|
|
3484
|
+
if (critic.verdict !== "PASS") {
|
|
3485
|
+
throw failClosed("critic verdict is not PASS", "critic_invalid");
|
|
3486
|
+
}
|
|
3487
|
+
if (critic.judged_tree !== acceptedTree) {
|
|
3488
|
+
throw failClosed("critic judged_tree does not match accepted tree", "critic_tree_mismatch");
|
|
3489
|
+
}
|
|
3490
|
+
if (critic.role !== spec.role) {
|
|
3491
|
+
throw failClosed("critic role does not match designated review role", "critic_invalid");
|
|
3492
|
+
}
|
|
3493
|
+
return Object.freeze({
|
|
3494
|
+
kind,
|
|
3495
|
+
role: spec.role,
|
|
3496
|
+
harness: spec.harness,
|
|
3497
|
+
model: spec.model,
|
|
3498
|
+
assignment_id: bound.identity.assignment_id,
|
|
3499
|
+
record_dir: bound.identity.record_dir,
|
|
3500
|
+
completion: Object.freeze({
|
|
3501
|
+
path: bound.loaded.path,
|
|
3502
|
+
sha256: sha256Bytes(bound.loaded.bytes),
|
|
3503
|
+
}),
|
|
3504
|
+
verdict: "PASS",
|
|
3505
|
+
judged_tree: acceptedTree,
|
|
3506
|
+
});
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
function listDirectAssignmentDirs(taskDir, io) {
|
|
3510
|
+
let names;
|
|
3511
|
+
try {
|
|
3512
|
+
names = io.readdirSync(taskDir);
|
|
3513
|
+
} catch (error) {
|
|
3514
|
+
throw failClosed(`cannot read task_dir: ${error?.message ?? error}`, "unknown");
|
|
3515
|
+
}
|
|
3516
|
+
const dirs = [];
|
|
3517
|
+
for (const name of names) {
|
|
3518
|
+
const entry = typeof name === "string" ? name : name?.name;
|
|
3519
|
+
if (typeof entry !== "string" || TASK_DIR_SKIP_NAMES.includes(entry)) continue;
|
|
3520
|
+
if (!IDENTITY.test(entry)) continue;
|
|
3521
|
+
const path = join(taskDir, entry);
|
|
3522
|
+
const stat = lstatOrNull(path, io.lstatSync);
|
|
3523
|
+
if (!stat || !stat.isDirectory()) continue;
|
|
3524
|
+
dirs.push({ assignment_id: entry, path: comparablePath(path, io.realpathSync, io.lstatSync) });
|
|
3525
|
+
}
|
|
3526
|
+
return dirs;
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
function historicalPlanRef(manifest, taskDir) {
|
|
3530
|
+
const planRef = manifest.plan_ref;
|
|
3531
|
+
if (!isPlainObject(planRef) || !PLAN_REF_KINDS.includes(planRef.kind)) {
|
|
3532
|
+
throw failClosed(`stored plan_ref.kind must be ${PLAN_REF_KINDS.join(" or ")}`, "plan_mismatch");
|
|
3533
|
+
}
|
|
3534
|
+
if (planRef.kind === "current") {
|
|
3535
|
+
closedObject(planRef, ["kind", "path", "sha256"], "historical plan_ref", [], "plan_mismatch");
|
|
3536
|
+
return Object.freeze({
|
|
3537
|
+
kind: "current",
|
|
3538
|
+
path: resolvePath(taskDir, requireNonemptyString(planRef.path, "historical plan_ref.path", "plan_mismatch")),
|
|
3539
|
+
sha256: requireHex(planRef.sha256, "historical plan_ref.sha256", HEX64, "plan_mismatch"),
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3542
|
+
closedObject(planRef, ["kind", "reason"], "historical plan_ref", [], "plan_mismatch");
|
|
3543
|
+
return Object.freeze({
|
|
3544
|
+
kind: "bootstrap",
|
|
3545
|
+
reason: requireNonemptyString(planRef.reason, "historical plan_ref.reason", "plan_mismatch"),
|
|
3546
|
+
});
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
function normalizeHistoricalBinding(binding, io, code = "attribution_invalid") {
|
|
3550
|
+
const closed = closedObject(
|
|
3551
|
+
binding,
|
|
3552
|
+
["task_dir", "cwd", "record_dir", "output_dir"],
|
|
3553
|
+
"historical binding",
|
|
3554
|
+
[],
|
|
3555
|
+
code,
|
|
3556
|
+
);
|
|
3557
|
+
return Object.fromEntries(Object.entries(closed).map(([key, path]) => {
|
|
3558
|
+
if (typeof path !== "string" || !nodePath.isAbsolute(path)) {
|
|
3559
|
+
throw failClosed("invalid historical path", code);
|
|
3560
|
+
}
|
|
3561
|
+
return [key, comparablePath(path, io.realpathSync, io.lstatSync)];
|
|
3562
|
+
}));
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
function loadHistoricallyBoundAssignment(recordDir, expectedTaskDir, io) {
|
|
3566
|
+
const loaded = loadWitnessCompletion(recordDir, io);
|
|
3567
|
+
const stored = loadStoredManifest(recordDir, loaded.completion, io);
|
|
3568
|
+
const identity = historicalAssignmentIdentity(stored.manifest, recordDir, io);
|
|
3569
|
+
if (!samePath(identity.task_dir, expectedTaskDir, io.realpathSync)) {
|
|
3570
|
+
throw failClosed("stored assignment task_dir does not match --task-dir", "witness_binding_invalid");
|
|
3571
|
+
}
|
|
3572
|
+
assertCompletionMatchesStored({
|
|
3573
|
+
...loaded.completion,
|
|
3574
|
+
binding: normalizeHistoricalBinding(loaded.completion.binding, io),
|
|
3575
|
+
}, identity, io);
|
|
3576
|
+
const plan = bindWitnessPlan(loaded.completion, {
|
|
3577
|
+
plan_ref: historicalPlanRef(stored.manifest, identity.task_dir),
|
|
3578
|
+
}, io);
|
|
3579
|
+
return { loaded, stored, identity, plan, completion: loaded.completion };
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
function tryLoadOwnedReview(recordDir, expectedTaskDir, expectedTaskId, io) {
|
|
3583
|
+
try {
|
|
3584
|
+
// BLOCK discovery binds historical identity, not live plan/cwd currency.
|
|
3585
|
+
const bound = loadHistoricallyBoundAssignment(recordDir, expectedTaskDir, io);
|
|
3586
|
+
if (bound.identity.task_id !== expectedTaskId) return null;
|
|
3587
|
+
if (bound.identity.assignment_id !== basename(recordDir)) return null;
|
|
3588
|
+
if (!REVIEW_KINDS.includes(bound.identity.kind)) return null;
|
|
3589
|
+
return bound;
|
|
3590
|
+
} catch {
|
|
3591
|
+
return null;
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
function reworkAncestorIds(assignmentId, byId) {
|
|
3596
|
+
const seen = new Set();
|
|
3597
|
+
let current = assignmentId;
|
|
3598
|
+
while (current && !seen.has(current)) {
|
|
3599
|
+
seen.add(current);
|
|
3600
|
+
current = byId.get(current)?.identity?.rework_of;
|
|
3601
|
+
}
|
|
3602
|
+
return seen;
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
function assertNoUnresolvedBlock(taskDir, critics, acceptedTree, expectedTaskId, io, criticSpecs = REQUIRED_ACCEPT_CRITICS) {
|
|
3606
|
+
const dirs = listDirectAssignmentDirs(taskDir, io);
|
|
3607
|
+
const reviews = [];
|
|
3608
|
+
for (const dir of dirs) {
|
|
3609
|
+
const bound = tryLoadOwnedReview(dir.path, taskDir, expectedTaskId, io);
|
|
3610
|
+
if (!bound) continue;
|
|
3611
|
+
reviews.push(bound);
|
|
3612
|
+
}
|
|
3613
|
+
const byId = new Map(reviews.map((item) => [item.identity.assignment_id, item]));
|
|
3614
|
+
for (const kind of Object.keys(criticSpecs)) {
|
|
3615
|
+
const supplied = critics.find((item) => item.kind === kind);
|
|
3616
|
+
if (!supplied) {
|
|
3617
|
+
throw failClosed(`missing designated ${kind} critic`, "critic_invalid");
|
|
3618
|
+
}
|
|
3619
|
+
const ancestors = reworkAncestorIds(supplied.assignment_id, byId);
|
|
3620
|
+
for (const review of reviews) {
|
|
3621
|
+
if (review.identity.kind !== kind) continue;
|
|
3622
|
+
const critic = review.completion.critic;
|
|
3623
|
+
if (!isPlainObject(critic) || critic.kind !== "review" || critic.verdict !== "BLOCK") continue;
|
|
3624
|
+
if (critic.judged_tree !== acceptedTree) continue;
|
|
3625
|
+
if (ancestors.has(review.identity.assignment_id) && review.identity.assignment_id !== supplied.assignment_id) {
|
|
3626
|
+
continue;
|
|
3627
|
+
}
|
|
3628
|
+
throw failClosed("unresolved critic BLOCK remains for the accepted tree", "critic_block");
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
function writeAcceptedRecord(taskDir, record, io) {
|
|
3634
|
+
const path = join(taskDir, ACCEPTED_FILENAME);
|
|
3635
|
+
if (lstatOrNull(path, io.lstatSync)) {
|
|
3636
|
+
throw failClosed("accepted.json already exists", "accepted_exists");
|
|
3637
|
+
}
|
|
3638
|
+
const body = `${JSON.stringify(record, undefined, 2)}\n`;
|
|
3639
|
+
try {
|
|
3640
|
+
writePrivate(path, body, io, "wx");
|
|
3641
|
+
} catch (error) {
|
|
3642
|
+
if (error && error.code === "EEXIST") {
|
|
3643
|
+
throw failClosed("accepted.json already exists", "accepted_exists");
|
|
3644
|
+
}
|
|
3645
|
+
throw failClosed(`cannot write accepted.json: ${error?.message ?? error}`, "record_write_failed");
|
|
3646
|
+
}
|
|
3647
|
+
return Object.freeze({ ...record, path, sha256: sha256Bytes(Buffer.from(body)) });
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
function logAcceptError(taskDir, error, io) {
|
|
3651
|
+
if (!taskDir) return;
|
|
3652
|
+
const code = RUNNER_CODES.includes(error?.runnerCode) ? error.runnerCode : "unknown";
|
|
3653
|
+
try {
|
|
3654
|
+
appendRunnerError(taskDir, {
|
|
3655
|
+
step: "accept",
|
|
3656
|
+
code,
|
|
3657
|
+
message: error?.message ?? "accept failed",
|
|
3658
|
+
}, io);
|
|
3659
|
+
} catch {
|
|
3660
|
+
// Public refusal already carries the bounded code.
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
export async function acceptAssignment(request, deps = {}) {
|
|
3665
|
+
const io = ioDeps(deps);
|
|
3666
|
+
let taskDir = null;
|
|
3667
|
+
try {
|
|
3668
|
+
const taskDirValue = request?.taskDir ?? request?.task_dir;
|
|
3669
|
+
const commitValue = request?.commit;
|
|
3670
|
+
const writerRecordValue = request?.recordDir ?? request?.record_dir;
|
|
3671
|
+
const criticValues = request?.critics;
|
|
3672
|
+
let policy;
|
|
3673
|
+
try {
|
|
3674
|
+
policy = getRosterPolicy(deps);
|
|
3675
|
+
} catch {
|
|
3676
|
+
policy = null;
|
|
3677
|
+
}
|
|
3678
|
+
const criticSpecs = resolveRequiredCritics(policy);
|
|
3679
|
+
const requiredKinds = Object.keys(criticSpecs);
|
|
3680
|
+
if (!Array.isArray(criticValues) || criticValues.length !== requiredKinds.length) {
|
|
3681
|
+
throw failClosed(`accept requires exactly ${requiredKinds.length} --critic record directories`, "manifest_invalid");
|
|
3682
|
+
}
|
|
3683
|
+
taskDir = requireAbsoluteExistingDir(taskDirValue, "--task-dir", "manifest_invalid", io);
|
|
3684
|
+
const writerDir = assertCanonicalRecordDir(taskDir, writerRecordValue, "--record-dir", io);
|
|
3685
|
+
const criticDirs = criticValues.map((value, index) => (
|
|
3686
|
+
assertCanonicalRecordDir(taskDir, value, `--critic[${index}]`, io)
|
|
3687
|
+
));
|
|
3688
|
+
if (new Set([writerDir, ...criticDirs]).size !== (1 + requiredKinds.length)) {
|
|
3689
|
+
throw failClosed("writer and critic record directories must be distinct", "critic_invalid");
|
|
3690
|
+
}
|
|
3691
|
+
const writer = loadBoundAssignment(writerDir, taskDir, io);
|
|
3692
|
+
assertEligibleWriter(writer, policy);
|
|
3693
|
+
const witness = loadLatestPassedWitness(
|
|
3694
|
+
writerDir,
|
|
3695
|
+
writer.identity,
|
|
3696
|
+
writer.stored,
|
|
3697
|
+
io,
|
|
3698
|
+
);
|
|
3699
|
+
const witnessedTree = assertReceiptMatchesWriter(
|
|
3700
|
+
witness.receipt,
|
|
3701
|
+
writer.identity,
|
|
3702
|
+
writer.stored,
|
|
3703
|
+
writer.loaded,
|
|
3704
|
+
writer.plan,
|
|
3705
|
+
io,
|
|
3706
|
+
);
|
|
3707
|
+
const plan = assertCurrentPlanAgreement(writer.identity, writer.completion, witness.receipt, io);
|
|
3708
|
+
const proven = proveAcceptedCommit(writer.identity.cwd, commitValue, io.spawnSync);
|
|
3709
|
+
if (proven.tree !== witnessedTree) {
|
|
3710
|
+
throw failClosed("commit tree does not equal the latest passed witness index_tree", "commit_tree_mismatch");
|
|
3711
|
+
}
|
|
3712
|
+
const criticEvidence = criticDirs.map((dir) => {
|
|
3713
|
+
const bound = loadBoundAssignment(dir, taskDir, io);
|
|
3714
|
+
return assertEligibleCritic(bound, proven.tree, io, writer, criticSpecs);
|
|
3715
|
+
});
|
|
3716
|
+
const kinds = criticEvidence.map((item) => item.kind);
|
|
3717
|
+
if (new Set(kinds).size !== requiredKinds.length) {
|
|
3718
|
+
throw failClosed(`critics must be distinct ${requiredKinds.join(" and ")} roles`, "critic_invalid");
|
|
3719
|
+
}
|
|
3720
|
+
for (const kind of requiredKinds) {
|
|
3721
|
+
if (!kinds.includes(kind)) {
|
|
3722
|
+
throw failClosed(`missing designated ${kind} critic`, "critic_invalid");
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
if (policy?.routes) {
|
|
3726
|
+
const writerMatch = findLineupRoute(policy, "writer", writer.identity.harness, writer.identity.model);
|
|
3727
|
+
const writerVendor = writerMatch ? canonicalVendor(writerMatch.route.vendor) : null;
|
|
3728
|
+
const criticVendors = Object.values(criticSpecs).map((spec) => canonicalVendor(spec.vendor));
|
|
3729
|
+
if (new Set(criticVendors).size !== criticVendors.length) {
|
|
3730
|
+
throw failClosed("critics must have independent vendors", "critic_invalid");
|
|
3731
|
+
}
|
|
3732
|
+
if (writerVendor && criticVendors.includes(writerVendor)) {
|
|
3733
|
+
throw failClosed("writer and critics must have independent vendors", "critic_invalid");
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
assertNoUnresolvedBlock(taskDir, criticEvidence, proven.tree, writer.identity.task_id, io, criticSpecs);
|
|
3737
|
+
const observed = closedObserved(
|
|
3738
|
+
observedId(request.observedPr ?? request.observed_pr, "--observed-pr"),
|
|
3739
|
+
observedId(request.observedCi ?? request.observed_ci, "--observed-ci"),
|
|
3740
|
+
);
|
|
3741
|
+
const acceptedAt = new Date((deps.now ?? Date.now)()).toISOString();
|
|
3742
|
+
const writerEvidence = Object.freeze({
|
|
3743
|
+
assignment_id: writer.identity.assignment_id,
|
|
3744
|
+
record_dir: writer.identity.record_dir,
|
|
3745
|
+
kind: writer.identity.kind,
|
|
3746
|
+
completion: Object.freeze({
|
|
3747
|
+
path: writer.loaded.path,
|
|
3748
|
+
sha256: sha256Bytes(writer.loaded.bytes),
|
|
3749
|
+
}),
|
|
3750
|
+
manifest: Object.freeze({
|
|
3751
|
+
path: writer.stored.path,
|
|
3752
|
+
sha256: writer.stored.sha256,
|
|
3753
|
+
}),
|
|
3754
|
+
receipt: Object.freeze({
|
|
3755
|
+
id: witness.receipt.receipt_id,
|
|
3756
|
+
path: witness.path,
|
|
3757
|
+
sha256: witness.sha256,
|
|
3758
|
+
}),
|
|
3759
|
+
});
|
|
3760
|
+
closedObject(writerEvidence, ACCEPTED_WRITER_KEYS, "accepted.writer", [], "unknown");
|
|
3761
|
+
for (const critic of criticEvidence) {
|
|
3762
|
+
closedObject(critic, ACCEPTED_CRITIC_KEYS, "accepted.critic", [], "unknown");
|
|
3763
|
+
closedObject(critic.completion, ACCEPTED_LINK_KEYS, "accepted.critic.completion", [], "unknown");
|
|
3764
|
+
}
|
|
3765
|
+
closedObject(writerEvidence.completion, ACCEPTED_LINK_KEYS, "accepted.writer.completion", [], "unknown");
|
|
3766
|
+
closedObject(writerEvidence.manifest, ACCEPTED_LINK_KEYS, "accepted.writer.manifest", [], "unknown");
|
|
3767
|
+
closedObject(writerEvidence.receipt, ACCEPTED_RECEIPT_KEYS, "accepted.writer.receipt", [], "unknown");
|
|
3768
|
+
const record = Object.freeze({
|
|
3769
|
+
schema: ACCEPTED_SCHEMA,
|
|
3770
|
+
task_id: writer.identity.task_id,
|
|
3771
|
+
commit: proven.commit,
|
|
3772
|
+
tree: proven.tree,
|
|
3773
|
+
writer: writerEvidence,
|
|
3774
|
+
plan,
|
|
3775
|
+
critics: Object.freeze(criticEvidence),
|
|
3776
|
+
acceptedAt,
|
|
3777
|
+
observed,
|
|
3778
|
+
note: ACCEPTED_NOTE,
|
|
3779
|
+
});
|
|
3780
|
+
closedObject(record, ACCEPTED_KEYS, "accepted", [], "unknown");
|
|
3781
|
+
return writeAcceptedRecord(taskDir, record, io);
|
|
3782
|
+
} catch (error) {
|
|
3783
|
+
logAcceptError(taskDir, error, io);
|
|
3784
|
+
throw error;
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3788
|
+
// Cost observations never become native evidence. Nullable measurements mean
|
|
3789
|
+
// unknown; cumulative token counts do not claim context-window occupancy.
|
|
3790
|
+
const COST_KEYS = ["schema", "task_id", "assignment_id", "kind", "route", "status", "timing", "usage", "sources", "note", "accounting", "cost_only", "rework", "rework_of"];
|
|
3791
|
+
const COST_USAGE_KEYS = ["input_tokens", "output_tokens", "cache_read_tokens", "cache_write_tokens", "reasoning_tokens", "context_tokens", "token_basis", "cost_basis", "cost_usd", "estimate_usd", "partial"];
|
|
3792
|
+
|
|
3793
|
+
function observationFailure(error, fallback = "observation_invalid") {
|
|
3794
|
+
// Notes, paths and arbitrary imported keys must not reach CLI diagnostics.
|
|
3795
|
+
return failClosed("assignment observation refused", error?.runnerCode ?? fallback);
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
function observationTask(taskDir, io) {
|
|
3799
|
+
const dir = requireAbsoluteExistingDir(taskDir, "task_dir", "record_outside_task", io);
|
|
3800
|
+
requireIdentity(basename(dir), "task_id", "record_outside_task");
|
|
3801
|
+
return dir;
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
function privateRecord(path, io, code = "observation_invalid") {
|
|
3805
|
+
const stat = lstatOrNull(path, io.lstatSync);
|
|
3806
|
+
if (!stat?.isFile()) throw failClosed("expected a regular record file", code);
|
|
3807
|
+
const bytes = io.readFileSync(path);
|
|
3808
|
+
return { path, bytes, sha256: sha256Bytes(bytes), value: parseJsonFile(path, bytes, "record", code) };
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
function validateCostObservation(value, taskDir, io, verifySources = true) {
|
|
3812
|
+
closedObject(value, COST_KEYS, "cost observation", ["rework_of"], "observation_invalid");
|
|
3813
|
+
if (value.schema !== COST_OBSERVATION_SCHEMA || value.cost_only !== true || value.task_id !== basename(taskDir)) {
|
|
3814
|
+
throw failClosed("cost observation identity is invalid", "observation_invalid");
|
|
3815
|
+
}
|
|
3816
|
+
requireIdentity(value.assignment_id, "assignment_id", "observation_invalid");
|
|
3817
|
+
if (![...ASSIGNMENT_KINDS, "unknown"].includes(value.kind)) throw failClosed("invalid observed kind", "observation_invalid");
|
|
3818
|
+
closedObject(value.route, ["harness", "provider", "model", "effort"], "route", [], "observation_invalid");
|
|
3819
|
+
for (const item of Object.values(value.route)) {
|
|
3820
|
+
if (typeof item !== "string" || !item.trim() || item.length > 256) throw failClosed("invalid observed route", "observation_invalid");
|
|
3821
|
+
}
|
|
3822
|
+
if (!["completed", "failed", "interrupted", "unknown"].includes(value.status)) throw failClosed("invalid observed status", "observation_invalid");
|
|
3823
|
+
closedObject(value.timing, ["started_at", "finished_at", "latency_ms"], "timing", [], "observation_invalid");
|
|
3824
|
+
for (const key of ["started_at", "finished_at"]) {
|
|
3825
|
+
const time = value.timing[key];
|
|
3826
|
+
if (time !== null && (typeof time !== "string" || !/^\d{4}-\d{2}-\d{2}T.*Z$/.test(time) || !Number.isFinite(Date.parse(time)))) {
|
|
3827
|
+
throw failClosed("invalid observed timestamp", "observation_invalid");
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
if (value.timing.started_at !== null && value.timing.finished_at !== null && Date.parse(value.timing.finished_at) < Date.parse(value.timing.started_at)) {
|
|
3831
|
+
throw failClosed("reversed observed timestamps", "observation_invalid");
|
|
3832
|
+
}
|
|
3833
|
+
closedObject(value.usage, COST_USAGE_KEYS, "usage", [], "observation_invalid");
|
|
3834
|
+
for (const key of COST_USAGE_KEYS.filter((key) => key.endsWith("_tokens"))) {
|
|
3835
|
+
const count = value.usage[key];
|
|
3836
|
+
if (count !== null && (!Number.isSafeInteger(count) || count < 0)) throw failClosed("invalid token count", "observation_invalid");
|
|
3837
|
+
}
|
|
3838
|
+
for (const amount of [value.timing.latency_ms, value.usage.cost_usd, value.usage.estimate_usd]) {
|
|
3839
|
+
if (amount !== null && (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0)) throw failClosed("invalid measurement", "observation_invalid");
|
|
3840
|
+
}
|
|
3841
|
+
const usage = value.usage;
|
|
3842
|
+
if (usage.token_basis !== TOKEN_BASIS || !COST_BASIS.includes(usage.cost_basis) || typeof usage.partial !== "boolean") {
|
|
3843
|
+
throw failClosed("invalid cost or token basis", "observation_invalid");
|
|
3844
|
+
}
|
|
3845
|
+
if ((usage.cost_basis !== "provider-reported" && usage.cost_usd !== null)
|
|
3846
|
+
|| (usage.cost_basis === "provider-reported" && usage.estimate_usd !== null)
|
|
3847
|
+
|| (usage.cost_basis === "unknown" && usage.estimate_usd !== null)) {
|
|
3848
|
+
throw failClosed("cost and estimate bases disagree", "observation_invalid");
|
|
3849
|
+
}
|
|
3850
|
+
if (typeof value.note !== "string") throw failClosed("private note must be text", "observation_invalid");
|
|
3851
|
+
if (value.rework !== null && typeof value.rework !== "boolean") throw failClosed("observed rework must be boolean or unknown", "observation_invalid");
|
|
3852
|
+
closedObject(value.accounting, ["included", "reason"], "accounting", [], "observation_invalid");
|
|
3853
|
+
if (typeof value.accounting.included !== "boolean" || typeof value.accounting.reason !== "string"
|
|
3854
|
+
|| (!value.accounting.included && !value.accounting.reason.trim())) throw failClosed("exclusions require a reason", "observation_invalid");
|
|
3855
|
+
if (value.rework_of !== undefined) {
|
|
3856
|
+
requireIdentity(value.rework_of, "rework_of", "observation_invalid");
|
|
3857
|
+
if (value.rework_of === value.assignment_id) throw failClosed("self rework is invalid", "observation_invalid");
|
|
3858
|
+
}
|
|
3859
|
+
if (!Array.isArray(value.sources) || value.sources.length === 0) throw failClosed("source provenance required", "source_invalid");
|
|
3860
|
+
const paths = new Set();
|
|
3861
|
+
for (const source of value.sources) {
|
|
3862
|
+
closedObject(source, ["path", "sha256"], "source", [], "source_invalid");
|
|
3863
|
+
if (typeof source.path !== "string" || !nodePath.isAbsolute(source.path)) throw failClosed("absolute source required", "source_invalid");
|
|
3864
|
+
requireHex(source.sha256, "source hash", /^[a-f0-9]{64}$/, "source_invalid");
|
|
3865
|
+
if (paths.has(source.path)) throw failClosed("duplicate source", "source_invalid");
|
|
3866
|
+
paths.add(source.path);
|
|
3867
|
+
if (verifySources) {
|
|
3868
|
+
const stat = lstatOrNull(source.path, io.lstatSync);
|
|
3869
|
+
if (!stat?.isFile() || sha256Bytes(io.readFileSync(source.path)) !== source.sha256) throw failClosed("source digest mismatch", "source_invalid");
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
return value;
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3875
|
+
function ensurePrivateDirectory(path, io) {
|
|
3876
|
+
const stat = lstatOrNull(path, io.lstatSync);
|
|
3877
|
+
if (stat) {
|
|
3878
|
+
if (!stat.isDirectory()) throw failClosed("unsafe history directory", "history_conflict");
|
|
3879
|
+
} else {
|
|
3880
|
+
io.mkdirSync(path, { mode: 0o700 });
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
function atomicPrivatePointer(path, value, io) {
|
|
3885
|
+
const stat = lstatOrNull(path, io.lstatSync);
|
|
3886
|
+
if (stat && !stat.isFile()) throw failClosed("unsafe latest pointer", "history_conflict");
|
|
3887
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
3888
|
+
writePrivate(temp, `${JSON.stringify(value, null, 2)}\n`, io);
|
|
3889
|
+
try { io.renameSync(temp, path); } finally { io.rmSync(temp, { force: true }); }
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
function withObservationLock(taskDir, io, run) {
|
|
3893
|
+
const lock = join(taskDir, ".observation-lock");
|
|
3894
|
+
try { io.mkdirSync(lock, { mode: 0o700 }); } catch { throw failClosed("observation writer is locked", "history_conflict"); }
|
|
3895
|
+
try { return run(); } finally { io.rmdirSync(lock); }
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
function assertSourcesNotRecorded(value, taskDir, io) {
|
|
3899
|
+
for (const item of listDirectAssignmentDirs(taskDir, io)) {
|
|
3900
|
+
const imported = join(item.path, "cost-observation.json");
|
|
3901
|
+
if (io.existsSync(imported)) {
|
|
3902
|
+
const prior = validateCostObservation(privateRecord(imported, io).value, taskDir, io, false);
|
|
3903
|
+
if (prior.assignment_id !== item.assignment_id) throw failClosed("foreign cost record", "observation_invalid");
|
|
3904
|
+
if (value.sources.some((source) => prior.sources.some((old) => old.sha256 === source.sha256
|
|
3905
|
+
|| comparablePath(old.path, io.realpathSync, io.lstatSync) === comparablePath(source.path, io.realpathSync, io.lstatSync)))) {
|
|
3906
|
+
throw failClosed("source already observed", "source_already_recorded");
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
// Canonical native records already own their usage. Their sidecars can
|
|
3910
|
+
// be external to task_dir, so check the stored output binding as well.
|
|
3911
|
+
if (io.existsSync(join(item.path, "manifest.json"))) {
|
|
3912
|
+
let manifest;
|
|
3913
|
+
try { manifest = privateRecord(join(item.path, "manifest.json"), io).value; }
|
|
3914
|
+
catch { continue; }
|
|
3915
|
+
const identity = identifyAssignment(manifest, io);
|
|
3916
|
+
// Legacy bootstrap directories and probe artifacts are not canonical
|
|
3917
|
+
// native records. Their explicit cost imports retain source provenance.
|
|
3918
|
+
if (!identity || identity.record_dir !== item.path) continue;
|
|
3919
|
+
const roots = [item.path];
|
|
3920
|
+
if (identity.output_dir && !io.existsSync(join(item.path, "refusal.json"))) {
|
|
3921
|
+
try {
|
|
3922
|
+
roots.push(historicalAssignmentIdentity(manifest, item.path, io).output_dir);
|
|
3923
|
+
} catch {
|
|
3924
|
+
// Invalid manifests can be persisted before dispatch. They own
|
|
3925
|
+
// their record directory, not an arbitrary invalid output root.
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
for (const source of value.sources) {
|
|
3929
|
+
const actual = comparablePath(source.path, io.realpathSync, io.lstatSync);
|
|
3930
|
+
if (roots.some((root) => actual === root || actual.startsWith(`${root}${nodePath.sep}`))) {
|
|
3931
|
+
throw failClosed("native source already recorded", "source_already_recorded");
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
export function observeAssignmentCost(request, deps = {}) {
|
|
3939
|
+
const io = ioDeps(deps);
|
|
3940
|
+
try {
|
|
3941
|
+
const taskDir = observationTask(request.taskDir, io);
|
|
3942
|
+
const value = validateCostObservation(request.observation, taskDir, io);
|
|
3943
|
+
return withObservationLock(taskDir, io, () => {
|
|
3944
|
+
const recordDir = join(taskDir, value.assignment_id);
|
|
3945
|
+
if (lstatOrNull(recordDir, io.lstatSync)) throw failClosed("identity already taken", "identity_taken");
|
|
3946
|
+
assertSourcesNotRecorded(value, taskDir, io);
|
|
3947
|
+
// Reserve non-recursively. A failed write leaves the identity consumed.
|
|
3948
|
+
io.mkdirSync(recordDir, { mode: 0o700 });
|
|
3949
|
+
const path = join(recordDir, "cost-observation.json");
|
|
3950
|
+
const bytes = Buffer.from(`${JSON.stringify(value, null, 2)}\n`);
|
|
3951
|
+
writePrivate(path, bytes, io);
|
|
3952
|
+
return { schema: COST_OBSERVATION_SCHEMA, task_id: value.task_id, assignment_id: value.assignment_id, cost_only: true, path, sha256: sha256Bytes(bytes) };
|
|
3953
|
+
});
|
|
3954
|
+
} catch (error) { throw observationFailure(error); }
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
function historicalAssignmentIdentity(manifest, recordDir, io) {
|
|
3958
|
+
closedObject(manifest, ASSIGNMENT_KEYS, "historical manifest", OPTIONAL_ASSIGNMENT_KEYS, "attribution_invalid");
|
|
3959
|
+
const identity = identifyAssignment(manifest, io);
|
|
3960
|
+
if (!identity || identity.record_dir !== recordDir || !ASSIGNMENT_KINDS.includes(manifest.kind)
|
|
3961
|
+
|| typeof manifest.cwd !== "string" || !nodePath.isAbsolute(manifest.cwd)) {
|
|
3962
|
+
throw failClosed("invalid historical identity", "attribution_invalid");
|
|
3963
|
+
}
|
|
3964
|
+
for (const key of ["harness", "model", "effort", "permission", "output_dir"]) {
|
|
3965
|
+
requireNonemptyString(manifest[key], key, "attribution_invalid");
|
|
3966
|
+
}
|
|
3967
|
+
const output = comparableOutputBinding(identity.output_dir, identity.task_dir, recordDir, manifest.cwd, io.lstatSync, io.realpathSync);
|
|
3968
|
+
// Attribution annotates immutable past facts; it does not admit a new
|
|
3969
|
+
// assignment or authorize a gate. Neither plan currency nor cwd liveness
|
|
3970
|
+
// is relevant, and a retired route can still receive historical notes.
|
|
3971
|
+
return { ...manifest, ...identity, role: KIND_ROLES[manifest.kind],
|
|
3972
|
+
cwd: comparablePath(manifest.cwd, io.realpathSync, io.lstatSync), output_dir: output.out };
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
function attributionSubject(taskDir, recordDir, io) {
|
|
3976
|
+
const cost = join(recordDir, "cost-observation.json");
|
|
3977
|
+
if (io.existsSync(cost)) {
|
|
3978
|
+
const loaded = privateRecord(cost, io);
|
|
3979
|
+
const value = validateCostObservation(loaded.value, taskDir, io, false);
|
|
3980
|
+
if (value.assignment_id !== basename(recordDir)) throw failClosed("foreign cost record", "attribution_invalid");
|
|
3981
|
+
return { path: loaded.path, sha256: loaded.sha256 };
|
|
3982
|
+
}
|
|
3983
|
+
const name = ["completion.json", "refusal.json", "pre-dispatch.json"].find((file) => io.existsSync(join(recordDir, file)));
|
|
3984
|
+
if (!name) throw failClosed("assignment has no bound record", "attribution_invalid");
|
|
3985
|
+
const loaded = privateRecord(join(recordDir, name), io);
|
|
3986
|
+
const manifest = privateRecord(join(recordDir, "manifest.json"), io);
|
|
3987
|
+
const identity = identifyAssignment(manifest.value, io);
|
|
3988
|
+
const value = loaded.value;
|
|
3989
|
+
const expected = { "completion.json": COMPLETION_SCHEMA, "refusal.json": REFUSAL_SCHEMA, "pre-dispatch.json": ASSIGNMENT_DISPATCH_SCHEMA }[name];
|
|
3990
|
+
if (!identity || identity.task_dir !== taskDir || identity.record_dir !== recordDir
|
|
3991
|
+
|| value.schema !== expected || value.task_id !== identity.task_id || value.assignment_id !== identity.assignment_id
|
|
3992
|
+
|| (value.manifest?.sha256 ?? value.manifest_sha256) !== manifest.sha256) {
|
|
3993
|
+
throw failClosed("assignment identity or digest mismatch", "attribution_invalid");
|
|
3994
|
+
}
|
|
3995
|
+
if (name === "completion.json") {
|
|
3996
|
+
const stored = historicalAssignmentIdentity(manifest.value, recordDir, io);
|
|
3997
|
+
assertCompletionMatchesStored({
|
|
3998
|
+
...value,
|
|
3999
|
+
binding: normalizeHistoricalBinding(value.binding, io),
|
|
4000
|
+
}, stored, io);
|
|
4001
|
+
} else if (name === "refusal.json") {
|
|
4002
|
+
if (value.provider_calls !== 0 || !samePath(value.binding?.task_dir ?? "", taskDir, io.realpathSync)
|
|
4003
|
+
|| !samePath(value.binding?.record_dir ?? "", recordDir, io.realpathSync)) throw failClosed("foreign refusal", "attribution_invalid");
|
|
4004
|
+
} else {
|
|
4005
|
+
historicalAssignmentIdentity(manifest.value, recordDir, io);
|
|
4006
|
+
}
|
|
4007
|
+
return { path: loaded.path, sha256: loaded.sha256 };
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
export function attributeAssignment(request, deps = {}) {
|
|
4011
|
+
const io = ioDeps(deps);
|
|
4012
|
+
try {
|
|
4013
|
+
const taskDir = observationTask(request.taskDir, io);
|
|
4014
|
+
const recordDir = assertCanonicalRecordDir(taskDir, request.recordDir, "record_dir", io);
|
|
4015
|
+
if (!ATTRIBUTION_CLASSES.includes(request.classification) || typeof request.explanation !== "string" || !request.explanation.trim()) {
|
|
4016
|
+
throw failClosed("attribution requires a class and private explanation", "attribution_invalid");
|
|
4017
|
+
}
|
|
4018
|
+
const subject = attributionSubject(taskDir, recordDir, io);
|
|
4019
|
+
return withObservationLock(taskDir, io, () => {
|
|
4020
|
+
const dir = join(recordDir, "attribution");
|
|
4021
|
+
const history = join(dir, "history");
|
|
4022
|
+
ensurePrivateDirectory(dir, io);
|
|
4023
|
+
ensurePrivateDirectory(history, io);
|
|
4024
|
+
const latestPath = join(dir, "latest.json");
|
|
4025
|
+
let previous = null;
|
|
4026
|
+
if (lstatOrNull(latestPath, io.lstatSync)) {
|
|
4027
|
+
const latest = privateRecord(latestPath, io, "history_conflict").value;
|
|
4028
|
+
const priorPath = join(history, `${requireIdentity(latest.id, "attribution id", "history_conflict")}.json`);
|
|
4029
|
+
const prior = privateRecord(priorPath, io, "history_conflict");
|
|
4030
|
+
if (prior.sha256 !== latest.sha256 || prior.value.task_id !== basename(taskDir)
|
|
4031
|
+
|| prior.value.assignment_id !== basename(recordDir)) throw failClosed("invalid history pointer", "history_conflict");
|
|
4032
|
+
previous = { id: latest.id, sha256: prior.sha256 };
|
|
4033
|
+
}
|
|
4034
|
+
const id = `a-${randomUUID()}`;
|
|
4035
|
+
const value = { schema: ATTRIBUTION_SCHEMA, id, task_id: basename(taskDir), assignment_id: basename(recordDir),
|
|
4036
|
+
classification: request.classification, recorded_at: new Date((io.now ?? Date.now)()).toISOString(),
|
|
4037
|
+
subject, previous, explanation: request.explanation };
|
|
4038
|
+
const bytes = Buffer.from(`${JSON.stringify(value, null, 2)}\n`);
|
|
4039
|
+
writePrivate(join(history, `${id}.json`), bytes, io);
|
|
4040
|
+
const latest = { schema: ATTRIBUTION_SCHEMA, id, classification: value.classification, sha256: sha256Bytes(bytes) };
|
|
4041
|
+
atomicPrivatePointer(latestPath, latest, io);
|
|
4042
|
+
return latest;
|
|
4043
|
+
});
|
|
4044
|
+
} catch (error) { throw observationFailure(error, "attribution_invalid"); }
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
function observationCli(args) {
|
|
4048
|
+
const allowed = args[0] === "attribute"
|
|
4049
|
+
? ["--task-dir", "--record-dir", "--class", "--explanation-file"]
|
|
4050
|
+
: ["--task-dir", "--input"];
|
|
4051
|
+
const values = {};
|
|
4052
|
+
for (let i = 1; i < args.length; i += 2) {
|
|
4053
|
+
if (!allowed.includes(args[i]) || values[args[i]] !== undefined || typeof args[i + 1] !== "string") {
|
|
4054
|
+
throw failClosed("invalid observation arguments", "observation_invalid");
|
|
4055
|
+
}
|
|
4056
|
+
values[args[i]] = args[i + 1];
|
|
4057
|
+
}
|
|
4058
|
+
if (allowed.some((key) => values[key] === undefined)) throw failClosed("missing observation argument", "observation_invalid");
|
|
4059
|
+
return values;
|
|
4060
|
+
}
|
|
4061
|
+
|
|
4062
|
+
export const CHANGE_REPORT_SCHEMA = "kxm.change-report.v1";
|
|
4063
|
+
|
|
4064
|
+
function artifactRef(loaded) { return { path: loaded.path, sha256: loaded.sha256 }; }
|
|
4065
|
+
function finiteMeasurement(value) { return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null; }
|
|
4066
|
+
function publicRoute(value = {}) {
|
|
4067
|
+
return Object.fromEntries(["harness", "provider", "model", "effort", "role"].map((key) =>
|
|
4068
|
+
[key, typeof value[key] === "string" && value[key].length <= 256 ? value[key] : "unknown"]));
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
function observationHistories(recordDir, io) {
|
|
4072
|
+
const result = { witnesses: [], attributions: [], latest_witness: null, latest_attribution: null, issues: [] };
|
|
4073
|
+
for (const [kind, folder, schema] of [["witness", "witness", WITNESS_SCHEMA], ["attribution", "attribution", ATTRIBUTION_SCHEMA]]) {
|
|
4074
|
+
const dir = join(recordDir, folder);
|
|
4075
|
+
if (!lstatOrNull(dir, io.lstatSync)) continue;
|
|
4076
|
+
if (!io.lstatSync(dir).isDirectory()) { result.issues.push(`${kind}_directory_invalid`); continue; }
|
|
4077
|
+
const history = join(dir, "history");
|
|
4078
|
+
const records = kind === "witness" ? result.witnesses : result.attributions;
|
|
4079
|
+
if (lstatOrNull(history, io.lstatSync)?.isDirectory()) {
|
|
4080
|
+
for (const name of io.readdirSync(history).sort()) {
|
|
4081
|
+
if (!name.endsWith(".json")) continue;
|
|
4082
|
+
try {
|
|
4083
|
+
const loaded = privateRecord(join(history, name), io);
|
|
4084
|
+
const value = loaded.value;
|
|
4085
|
+
if (value.schema !== schema || value.assignment_id !== basename(recordDir) || value.task_id !== basename(dirname(recordDir))) throw new Error("foreign history");
|
|
4086
|
+
if (kind === "witness") {
|
|
4087
|
+
if (!WITNESS_RESULTS.includes(value.result) || `${value.receipt_id}.json` !== name) throw new Error("invalid witness");
|
|
4088
|
+
records.push({ ...artifactRef(loaded), id: value.receipt_id, result: value.result, duration_ms: finiteMeasurement(value.durationMs) });
|
|
4089
|
+
} else {
|
|
4090
|
+
if (!ATTRIBUTION_CLASSES.includes(value.classification) || `${value.id}.json` !== name) throw new Error("invalid attribution");
|
|
4091
|
+
records.push({ ...artifactRef(loaded), id: value.id, classification: value.classification, explanation_ref: { path: loaded.path, field: "explanation" } });
|
|
4092
|
+
}
|
|
4093
|
+
} catch { result.issues.push(`${kind}_history_invalid`); }
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
const latest = join(dir, "latest.json");
|
|
4097
|
+
if (lstatOrNull(latest, io.lstatSync)) {
|
|
4098
|
+
try {
|
|
4099
|
+
const value = privateRecord(latest, io).value;
|
|
4100
|
+
const id = kind === "witness" ? value.receipt_id : value.id;
|
|
4101
|
+
const found = records.find((item) => item.id === id && item.sha256 === value.sha256);
|
|
4102
|
+
if (!found) throw new Error("invalid latest");
|
|
4103
|
+
result[`latest_${kind}`] = found.id;
|
|
4104
|
+
} catch { result.issues.push(`${kind}_latest_invalid`); }
|
|
4105
|
+
} else if (records.length) result.issues.push(`${kind}_latest_missing`);
|
|
4106
|
+
}
|
|
4107
|
+
return result;
|
|
4108
|
+
}
|
|
4109
|
+
|
|
4110
|
+
function reportAssignment(dir, taskDir, io) {
|
|
4111
|
+
const base = { assignment_id: basename(dir), record_dir: dir };
|
|
4112
|
+
const cost = join(dir, "cost-observation.json");
|
|
4113
|
+
if (lstatOrNull(cost, io.lstatSync)) {
|
|
4114
|
+
const loaded = privateRecord(cost, io);
|
|
4115
|
+
const value = validateCostObservation(loaded.value, taskDir, io, false);
|
|
4116
|
+
if (value.assignment_id !== base.assignment_id) throw failClosed("foreign cost identity", "observation_invalid");
|
|
4117
|
+
return { ...base, type: "cost-observation", source: artifactRef(loaded), cost_only: true,
|
|
4118
|
+
included: value.accounting.included, exclusion_reason_ref: value.accounting.included ? null : { path: cost, field: "accounting.reason" },
|
|
4119
|
+
provenance: value.sources, private_note_ref: { path: cost, field: "note" }, route: publicRoute(value.route), kind: value.kind,
|
|
4120
|
+
status: value.status, rework: value.rework, rework_of: value.rework_of ?? null, timing: value.timing, usage: value.usage,
|
|
4121
|
+
verification: "ineligible", critic: null, history: observationHistories(dir, io) };
|
|
4122
|
+
}
|
|
4123
|
+
const manifestPath = join(dir, "manifest.json");
|
|
4124
|
+
if (!lstatOrNull(manifestPath, io.lstatSync)) return null;
|
|
4125
|
+
const manifest = privateRecord(manifestPath, io);
|
|
4126
|
+
const identity = identifyAssignment(manifest.value, io);
|
|
4127
|
+
if (!identity || identity.record_dir !== dir) return null;
|
|
4128
|
+
const hasRecord = ["completion.json", "refusal.json", "pre-dispatch.json"].some((name) => lstatOrNull(join(dir, name), io.lstatSync));
|
|
4129
|
+
const source = hasRecord ? attributionSubject(taskDir, dir, io) : artifactRef(manifest);
|
|
4130
|
+
const histories = observationHistories(dir, io);
|
|
4131
|
+
const row = { ...base, source, included: true, cost_only: false, kind: identity.kind,
|
|
4132
|
+
route: publicRoute({ ...manifest.value, role: KIND_ROLES[identity.kind] }), rework: Boolean(manifest.value.rework_of),
|
|
4133
|
+
rework_of: manifest.value.rework_of ?? null, history: histories };
|
|
4134
|
+
if (source.path.endsWith("completion.json")) {
|
|
4135
|
+
const value = privateRecord(source.path, io).value;
|
|
4136
|
+
const usage = value.usage ?? {};
|
|
4137
|
+
const basis = COST_BASIS.includes(usage.costBasis) ? usage.costBasis : "unknown";
|
|
4138
|
+
const latest = histories.witnesses.find((item) => item.id === histories.latest_witness);
|
|
4139
|
+
return { ...row, type: "native", route: publicRoute({ ...value.route, provider: value.route?.harness === "pi" ? piProviderOf(value.route?.model) : { claude: "anthropic", codex: "openai", grok: "xai" }[value.route?.harness] }), status: value.transport?.status ?? "unknown",
|
|
4140
|
+
recording: value.recording?.status ?? "unknown", model_claim: value.model_claim ?? null,
|
|
4141
|
+
critic: value.critic?.kind === "review" ? { verdict: value.critic.verdict, judged_tree: value.critic.judged_tree } : null,
|
|
4142
|
+
verification: latest?.result ?? "not-run", timing: { started_at: value.transport?.startedAt ?? null,
|
|
4143
|
+
finished_at: value.transport?.finishedAt ?? null, latency_ms: finiteMeasurement(value.transport?.latencyMs) },
|
|
4144
|
+
usage: { input_tokens: finiteMeasurement(usage.tokensIn), output_tokens: finiteMeasurement(usage.tokensOut),
|
|
4145
|
+
cache_read_tokens: finiteMeasurement(usage.cacheReadTokens), cache_write_tokens: finiteMeasurement(usage.cacheCreationTokens),
|
|
4146
|
+
reasoning_tokens: finiteMeasurement(usage.reasoningTokens), context_tokens: null, token_basis: "cumulative",
|
|
4147
|
+
cost_basis: basis, cost_usd: basis === "provider-reported" ? finiteMeasurement(usage.costUsd) : null,
|
|
4148
|
+
estimate_usd: ["list", "unmetered"].includes(basis) ? finiteMeasurement(usage.providerReportedCostUsd ?? usage.costUsd) : null,
|
|
4149
|
+
partial: usage.usagePartial === true || value.transport?.status !== "completed" } };
|
|
4150
|
+
}
|
|
4151
|
+
if (source.path.endsWith("refusal.json")) {
|
|
4152
|
+
const value = privateRecord(source.path, io).value;
|
|
4153
|
+
return { ...row, type: "refusal", status: "refused", code: value.code, provider_calls: 0,
|
|
4154
|
+
verification: "not-run", critic: null, timing: null, usage: null };
|
|
4155
|
+
}
|
|
4156
|
+
return { ...row, type: "missing-completion", status: "unknown", verification: "not-run", critic: null,
|
|
4157
|
+
timing: { started_at: privateRecord(source.path, io).value.timestamp ?? null, finished_at: null, latency_ms: null },
|
|
4158
|
+
usage: { cost_basis: "unknown", cost_usd: null, estimate_usd: null, partial: true } };
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4161
|
+
function sumMeasurements(values) {
|
|
4162
|
+
const known = values.map(finiteMeasurement).filter((value) => value !== null);
|
|
4163
|
+
return { total: known.length ? known.reduce((sum, value) => sum + value, 0) : null, known: known.length, unknown: values.length - known.length };
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
export function changeReport(request, deps = {}) {
|
|
4167
|
+
const io = ioDeps(deps);
|
|
4168
|
+
const taskDir = observationTask(request.taskDir, io);
|
|
4169
|
+
const assignments = [];
|
|
4170
|
+
const ignored = [];
|
|
4171
|
+
const issues = [];
|
|
4172
|
+
const dirs = listDirectAssignmentDirs(taskDir, io);
|
|
4173
|
+
const candidates = new Set(dirs.map((item) => item.assignment_id));
|
|
4174
|
+
for (const name of io.readdirSync(taskDir).sort()) {
|
|
4175
|
+
if (!candidates.has(name)) { ignored.push({ name, reason: "not_assignment_directory" }); continue; }
|
|
4176
|
+
try {
|
|
4177
|
+
const row = reportAssignment(join(taskDir, name), taskDir, io);
|
|
4178
|
+
if (row) assignments.push(row);
|
|
4179
|
+
else ignored.push({ name, reason: "no_canonical_assignment" });
|
|
4180
|
+
} catch {
|
|
4181
|
+
issues.push({ assignment_id: name, code: "record_invalid" });
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
const included = assignments.filter((row) => row.included);
|
|
4185
|
+
const metered = included.filter((row) => row.type !== "refusal");
|
|
4186
|
+
const timeValues = (key) => included.map((row) => row.timing?.[key])
|
|
4187
|
+
.filter((value) => typeof value === "string").map(Date.parse).filter(Number.isFinite);
|
|
4188
|
+
const starts = timeValues("started_at");
|
|
4189
|
+
const finishes = timeValues("finished_at");
|
|
4190
|
+
const timesComplete = metered.every((row) => row.timing?.started_at && row.timing?.finished_at);
|
|
4191
|
+
const costs = Object.fromEntries(COST_BASIS.map((basis) => {
|
|
4192
|
+
const rows = metered.filter((row) => row.usage.cost_basis === basis);
|
|
4193
|
+
return [basis, { assignments: rows.length, amount_basis: ["list", "unmetered"].includes(basis) ? "estimate" : basis,
|
|
4194
|
+
amount_usd: sumMeasurements(rows.map((row) => basis === "provider-reported" ? row.usage.cost_usd : row.usage.estimate_usd)) }];
|
|
4195
|
+
}));
|
|
4196
|
+
const efforts = new Map();
|
|
4197
|
+
for (const row of included) {
|
|
4198
|
+
const key = JSON.stringify([row.route.harness, row.route.model, row.kind, row.route.effort]);
|
|
4199
|
+
const value = efforts.get(key) ?? { route: row.route, kind: row.kind, attempts: 0, verified: 0, rework: 0, rework_unknown: 0 };
|
|
4200
|
+
value.attempts += 1; value.verified += Number(row.verification === "passed");
|
|
4201
|
+
value.rework += Number(row.rework === true); value.rework_unknown += Number(row.rework === null);
|
|
4202
|
+
efforts.set(key, value);
|
|
4203
|
+
}
|
|
4204
|
+
let acceptance = null;
|
|
4205
|
+
if (lstatOrNull(join(taskDir, ACCEPTED_FILENAME), io.lstatSync)) {
|
|
4206
|
+
try {
|
|
4207
|
+
const loaded = privateRecord(join(taskDir, ACCEPTED_FILENAME), io);
|
|
4208
|
+
const value = loaded.value;
|
|
4209
|
+
if (value.schema !== ACCEPTED_SCHEMA || value.task_id !== basename(taskDir)) throw new Error("foreign acceptance");
|
|
4210
|
+
acceptance = { source: artifactRef(loaded), commit: value.commit, recorded_tree: value.tree, observed: value.observed,
|
|
4211
|
+
note: ACCEPTED_NOTE, derived_tree: null, commit_tree_rechecked: false };
|
|
4212
|
+
try {
|
|
4213
|
+
const writerDir = assertCanonicalRecordDir(taskDir, value.writer?.record_dir, "writer", io);
|
|
4214
|
+
const writer = privateRecord(join(writerDir, "completion.json"), io).value;
|
|
4215
|
+
attributionSubject(taskDir, writerDir, io);
|
|
4216
|
+
const proven = proveAcceptedCommit(writer.binding.cwd, value.commit, io.spawnSync);
|
|
4217
|
+
acceptance.derived_tree = proven.tree;
|
|
4218
|
+
acceptance.commit_tree_rechecked = proven.tree === value.tree;
|
|
4219
|
+
if (!acceptance.commit_tree_rechecked) issues.push({ code: "accepted_tree_mismatch" });
|
|
4220
|
+
} catch { issues.push({ code: "accepted_commit_unavailable" }); }
|
|
4221
|
+
} catch { issues.push({ code: "acceptance_record_invalid" }); }
|
|
4222
|
+
}
|
|
4223
|
+
return { schema: CHANGE_REPORT_SCHEMA, task_id: basename(taskDir), generated_at: new Date((io.now ?? Date.now)()).toISOString(),
|
|
4224
|
+
assignments, ignored, issues, acceptance, totals: { scope: "recorded task assignments", included: included.length, excluded: assignments.length - included.length,
|
|
4225
|
+
not_dispatched: included.filter((row) => row.type === "refusal").length, costs,
|
|
4226
|
+
partial: issues.length > 0 || included.some((row) => row.history.issues.length > 0) || metered.some((row) => row.usage.partial || row.usage.cost_basis === "unknown" || row.usage.cost_usd === null && row.usage.estimate_usd === null),
|
|
4227
|
+
elapsed_ms: starts.length && finishes.length ? Math.max(0, Math.max(...finishes) - Math.min(...starts)) : null, elapsed_partial: !timesComplete,
|
|
4228
|
+
summed_native_latency_ms: sumMeasurements(metered.map((row) => row.timing?.latency_ms)),
|
|
4229
|
+
summed_witness_duration_ms: sumMeasurements(included.flatMap((row) => row.history.witnesses.map((item) => item.duration_ms))) },
|
|
4230
|
+
effort_observations: [...efforts.values()], ranking: "not-ranked", orchestration_usage: "unavailable",
|
|
4231
|
+
notes: ["Provider-reported amounts are not invoices; subscription estimates are not spend.",
|
|
4232
|
+
"Token counters are cumulative, not measured context occupancy. Missing measurements are not zero.",
|
|
4233
|
+
"History lists every retained receipt, including failed and orphaned entries. Private handoff notes are referenced, not printed.",
|
|
4234
|
+
"Observed PR/CI identifiers do not prove CI success or merge. No acceptance or role authority is created by this report."] };
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
export function writeCurrentPlan(request, deps = {}) {
|
|
4238
|
+
const io = ioDeps(deps);
|
|
4239
|
+
try {
|
|
4240
|
+
const taskDir = observationTask(request.taskDir, io);
|
|
4241
|
+
if (!Number.isSafeInteger(request.expectedGeneration) || request.expectedGeneration < 0) throw failClosed("expected generation required", "plan_ref_invalid");
|
|
4242
|
+
if (typeof request.plan !== "string" || !nodePath.isAbsolute(request.plan)) throw failClosed("absolute plan required", "plan_ref_invalid");
|
|
4243
|
+
requireHex(request.sha256, "proposed plan digest", HEX64, "plan_ref_invalid");
|
|
4244
|
+
requireHex(request.baseCommit, "base commit", HEX40, "plan_ref_invalid");
|
|
4245
|
+
return withObservationLock(taskDir, io, () => {
|
|
4246
|
+
const proposedStat = lstatOrNull(request.plan, io.lstatSync);
|
|
4247
|
+
if (!proposedStat?.isFile()) throw failClosed("regular plan file required", "plan_ref_invalid");
|
|
4248
|
+
const proposed = io.readFileSync(request.plan);
|
|
4249
|
+
if (sha256Bytes(proposed) !== request.sha256) throw failClosed("proposed plan hash mismatch", "plan_ref_invalid");
|
|
4250
|
+
const pointerPath = join(taskDir, PLAN_POINTER_FILENAME);
|
|
4251
|
+
if (samePath(request.plan, pointerPath, io.realpathSync)) throw failClosed("plan cannot be its own pointer", "plan_ref_invalid");
|
|
4252
|
+
if (request.settledDecisions !== undefined) requireStringArray(request.settledDecisions, "settled decisions", "plan_ref_invalid");
|
|
4253
|
+
let old = null;
|
|
4254
|
+
let oldPlan = null;
|
|
4255
|
+
if (lstatOrNull(pointerPath, io.lstatSync)) {
|
|
4256
|
+
old = privateRecord(pointerPath, io, "plan_ref_invalid");
|
|
4257
|
+
validatePointer(old.value);
|
|
4258
|
+
if (old.value.task_id !== basename(taskDir)) throw failClosed("foreign pointer", "plan_ref_invalid");
|
|
4259
|
+
const oldPath = resolvePath(taskDir, old.value.plan_path);
|
|
4260
|
+
const stat = lstatOrNull(oldPath, io.lstatSync);
|
|
4261
|
+
if (!stat?.isFile()) throw failClosed("old plan unavailable", "plan_ref_invalid");
|
|
4262
|
+
const bytes = io.readFileSync(oldPath);
|
|
4263
|
+
if (sha256Bytes(bytes) !== old.value.plan_sha256) throw failClosed("old plan hash mismatch", "plan_ref_invalid");
|
|
4264
|
+
oldPlan = bytes;
|
|
4265
|
+
}
|
|
4266
|
+
if ((old?.value.generation ?? 0) !== request.expectedGeneration) throw failClosed("stale plan generation", "history_conflict");
|
|
4267
|
+
const supersedes = [...(old?.value.supersedes ?? [])];
|
|
4268
|
+
if (old) {
|
|
4269
|
+
const history = join(taskDir, "plan-history");
|
|
4270
|
+
ensurePrivateDirectory(history, io);
|
|
4271
|
+
const archivedPlan = join(history, `generation-${old.value.generation}.md`);
|
|
4272
|
+
const archivedPointer = join(history, `generation-${old.value.generation}.json`);
|
|
4273
|
+
if (lstatOrNull(archivedPlan, io.lstatSync) || lstatOrNull(archivedPointer, io.lstatSync)) throw failClosed("plan history conflict", "history_conflict");
|
|
4274
|
+
writePrivate(archivedPlan, oldPlan, io);
|
|
4275
|
+
writePrivate(archivedPointer, old.bytes, io);
|
|
4276
|
+
supersedes.push({ generation: old.value.generation, plan_path: archivedPlan, plan_sha256: old.value.plan_sha256,
|
|
4277
|
+
base_commit: old.value.base_commit, updated_at: old.value.updated_at });
|
|
4278
|
+
}
|
|
4279
|
+
const value = { schema: PLAN_POINTER_SCHEMA, task_id: basename(taskDir), generation: request.expectedGeneration + 1,
|
|
4280
|
+
plan_path: comparablePath(request.plan, io.realpathSync, io.lstatSync), plan_sha256: request.sha256, base_commit: request.baseCommit,
|
|
4281
|
+
settled_decisions: request.settledDecisions ?? old?.value.settled_decisions ?? [], supersedes, updated_at: new Date((io.now ?? Date.now)()).toISOString() };
|
|
4282
|
+
validatePointer(value);
|
|
4283
|
+
atomicPrivatePointer(pointerPath, value, io);
|
|
4284
|
+
return value;
|
|
4285
|
+
});
|
|
4286
|
+
} catch (error) { throw observationFailure(error, "plan_ref_invalid"); }
|
|
4287
|
+
}
|
|
4288
|
+
|
|
4289
|
+
export async function main(argv = process.argv, io = { stdin: process.stdin, stdout: process.stdout, stderr: process.stderr }) {
|
|
4290
|
+
const args = argv.slice(2).filter((arg) => arg !== "--");
|
|
4291
|
+
if (["change-report", "plan-current"].includes(args[0])) {
|
|
4292
|
+
try {
|
|
4293
|
+
const flags = args[0] === "change-report" ? ["--task-dir"] : ["--task-dir", "--plan", "--sha256", "--base-commit", "--expected-generation"];
|
|
4294
|
+
const values = {};
|
|
4295
|
+
for (let i = 1; i < args.length; i += 2) {
|
|
4296
|
+
if (!flags.includes(args[i]) || values[args[i]] !== undefined || args[i + 1] === undefined) throw failClosed("invalid report/plan arguments", "manifest_invalid");
|
|
4297
|
+
values[args[i]] = args[i + 1];
|
|
4298
|
+
}
|
|
4299
|
+
if (flags.some((key) => values[key] === undefined)) throw failClosed("missing report/plan arguments", "manifest_invalid");
|
|
4300
|
+
const result = args[0] === "change-report" ? changeReport({ taskDir: values["--task-dir"] })
|
|
4301
|
+
: writeCurrentPlan({ taskDir: values["--task-dir"], plan: values["--plan"], sha256: values["--sha256"], baseCommit: values["--base-commit"], expectedGeneration: /^\d+$/.test(values["--expected-generation"]) ? Number(values["--expected-generation"]) : NaN });
|
|
4302
|
+
io.stdout.write(`${JSON.stringify(result, null, 2)}\n`); process.exitCode = 0; return result;
|
|
4303
|
+
} catch (error) {
|
|
4304
|
+
const failure = observationFailure(error); io.stderr.write(`${failure.runnerCode}: assignment report/plan refused\n`); process.exitCode = 1; throw failure;
|
|
4305
|
+
}
|
|
4306
|
+
}
|
|
4307
|
+
if (["attribute", "observe-cost"].includes(args[0])) {
|
|
4308
|
+
try {
|
|
4309
|
+
const values = observationCli(args);
|
|
4310
|
+
const source = values["--input"] ?? values["--explanation-file"];
|
|
4311
|
+
if (!nodePath.isAbsolute(source) || !lstatSync(source).isFile()) throw failClosed("regular input file required", "source_invalid");
|
|
4312
|
+
const text = readFileSync(source, "utf8");
|
|
4313
|
+
const result = args[0] === "attribute"
|
|
4314
|
+
? attributeAssignment({ taskDir: values["--task-dir"], recordDir: values["--record-dir"], classification: values["--class"], explanation: text })
|
|
4315
|
+
: observeAssignmentCost({ taskDir: values["--task-dir"], observation: JSON.parse(text) });
|
|
4316
|
+
io.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
4317
|
+
process.exitCode = 0;
|
|
4318
|
+
return result;
|
|
4319
|
+
} catch (error) {
|
|
4320
|
+
const failure = observationFailure(error);
|
|
4321
|
+
io.stderr.write(`${failure.runnerCode}: assignment observation refused\n`);
|
|
4322
|
+
process.exitCode = 1;
|
|
4323
|
+
throw failure;
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
if (args[0] === "accept") {
|
|
4327
|
+
try {
|
|
4328
|
+
const parsed = parseAcceptCli(args);
|
|
4329
|
+
const accepted = await acceptAssignment({
|
|
4330
|
+
taskDir: parsed.taskDir,
|
|
4331
|
+
commit: parsed.commit,
|
|
4332
|
+
recordDir: parsed.recordDir,
|
|
4333
|
+
critics: parsed.critics,
|
|
4334
|
+
observedPr: parsed.observedPr,
|
|
4335
|
+
observedCi: parsed.observedCi,
|
|
4336
|
+
}, {
|
|
4337
|
+
spawnSync: io.spawnSync ?? spawnSync,
|
|
4338
|
+
env: io.env,
|
|
4339
|
+
now: io.now,
|
|
4340
|
+
});
|
|
4341
|
+
io.stdout.write(`${JSON.stringify(accepted, undefined, 2)}\n`);
|
|
4342
|
+
process.exitCode = 0;
|
|
4343
|
+
return accepted;
|
|
4344
|
+
} catch (error) {
|
|
4345
|
+
const code = error?.runnerCode && RUNNER_CODES.includes(error.runnerCode)
|
|
4346
|
+
? error.runnerCode
|
|
4347
|
+
: "unknown";
|
|
4348
|
+
io.stderr.write(`${code}: ${error.message}\n`);
|
|
4349
|
+
process.exitCode = 1;
|
|
4350
|
+
throw error;
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
if (args.includes("--task-dir")) {
|
|
4354
|
+
throw failClosed("run --manifest does not accept --task-dir; task_dir comes from the manifest", "manifest_invalid");
|
|
4355
|
+
}
|
|
4356
|
+
if (args[0] === "observe") {
|
|
4357
|
+
if (args[1] !== "--record-dir" || typeof args[2] !== "string" || args.length !== 3) {
|
|
4358
|
+
throw failClosed(CLI_USAGE, "manifest_invalid");
|
|
4359
|
+
}
|
|
4360
|
+
const recordDir = args[2];
|
|
4361
|
+
if (!nodePath.isAbsolute(recordDir)) {
|
|
4362
|
+
throw failClosed("--record-dir must be an absolute path", "manifest_invalid");
|
|
4363
|
+
}
|
|
4364
|
+
try {
|
|
4365
|
+
await observeAssignment(recordDir);
|
|
4366
|
+
const resolutionPath = join(recordDir, "recording-resolved.json");
|
|
4367
|
+
const resolution = parseJsonFile(
|
|
4368
|
+
resolutionPath,
|
|
4369
|
+
readRegularFile(resolutionPath, readFileSync, existsSync, statSync),
|
|
4370
|
+
"recording resolution",
|
|
4371
|
+
);
|
|
4372
|
+
io.stdout.write(`${JSON.stringify(resolution, undefined, 2)}\n`);
|
|
4373
|
+
const unresolved = Object.values(resolution.steps ?? {}).some((step) => step === "failed");
|
|
4374
|
+
process.exitCode = unresolved ? 1 : 0;
|
|
4375
|
+
return resolution;
|
|
4376
|
+
} catch (error) {
|
|
4377
|
+
const code = error?.runnerCode && RUNNER_CODES.includes(error.runnerCode)
|
|
4378
|
+
? error.runnerCode
|
|
4379
|
+
: "unknown";
|
|
4380
|
+
io.stderr.write(`${code}: ${error.message}\n`);
|
|
4381
|
+
process.exitCode = 1;
|
|
4382
|
+
throw error;
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
if (args[0] === "witness") {
|
|
4386
|
+
if (args[1] !== "--record-dir" || typeof args[2] !== "string" || args.length !== 3) {
|
|
4387
|
+
throw failClosed(CLI_USAGE, "manifest_invalid");
|
|
4388
|
+
}
|
|
4389
|
+
const recordDir = args[2];
|
|
4390
|
+
if (!nodePath.isAbsolute(recordDir)) {
|
|
4391
|
+
throw failClosed("--record-dir must be an absolute path", "manifest_invalid");
|
|
4392
|
+
}
|
|
4393
|
+
try {
|
|
4394
|
+
const receipt = await witnessAssignment(recordDir, {
|
|
4395
|
+
spawnSync: io.spawnSync ?? spawnSync,
|
|
4396
|
+
env: io.env,
|
|
4397
|
+
now: io.now,
|
|
4398
|
+
});
|
|
4399
|
+
io.stdout.write(`${JSON.stringify(receipt, undefined, 2)}\n`);
|
|
4400
|
+
process.exitCode = receipt.result === "passed" ? 0 : 1;
|
|
4401
|
+
return receipt;
|
|
4402
|
+
} catch (error) {
|
|
4403
|
+
const code = error?.runnerCode && RUNNER_CODES.includes(error.runnerCode)
|
|
4404
|
+
? error.runnerCode
|
|
4405
|
+
: "unknown";
|
|
4406
|
+
io.stderr.write(`${code}: ${error.message}\n`);
|
|
4407
|
+
process.exitCode = 1;
|
|
4408
|
+
throw error;
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
if (args[0] !== "run" || args[1] !== "--manifest" || typeof args[2] !== "string" || args.length !== 3) {
|
|
4412
|
+
throw failClosed(CLI_USAGE, "manifest_invalid");
|
|
4413
|
+
}
|
|
4414
|
+
const manifestPath = args[2];
|
|
4415
|
+
if (!nodePath.isAbsolute(manifestPath)) {
|
|
4416
|
+
throw failClosed("--manifest must be an absolute path", "manifest_invalid");
|
|
4417
|
+
}
|
|
4418
|
+
const bytes = readFileSync(manifestPath);
|
|
4419
|
+
const manifest = parseJsonFile(manifestPath, bytes, "assignment manifest");
|
|
4420
|
+
try {
|
|
4421
|
+
const completion = await runAssignment(manifest, { manifestBytes: bytes });
|
|
4422
|
+
io.stdout.write(`${JSON.stringify(completion, undefined, 2)}\n`);
|
|
4423
|
+
process.exitCode = completion.transport?.ok === true && completion.recording?.status === "ok" ? 0 : 1;
|
|
4424
|
+
return completion;
|
|
4425
|
+
} catch (error) {
|
|
4426
|
+
const code = error?.runnerCode && RUNNER_CODES.includes(error.runnerCode)
|
|
4427
|
+
? error.runnerCode
|
|
4428
|
+
: "unknown";
|
|
4429
|
+
io.stderr.write(`${code}: ${error.message}\n`);
|
|
4430
|
+
process.exitCode = 1;
|
|
4431
|
+
throw error;
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
4434
|
+
|
|
4435
|
+
function invokedAsMain() {
|
|
4436
|
+
const self = fileURLToPath(import.meta.url);
|
|
4437
|
+
const argv1 = process.argv[1];
|
|
4438
|
+
if (!argv1) return false;
|
|
4439
|
+
return sameInvokedPath(argv1, self);
|
|
4440
|
+
}
|
|
4441
|
+
|
|
4442
|
+
if (invokedAsMain()) {
|
|
4443
|
+
main().catch((error) => {
|
|
4444
|
+
if (error?.failClosed && process.exitCode === 1) return;
|
|
4445
|
+
const code = error?.runnerCode && RUNNER_CODES.includes(error.runnerCode)
|
|
4446
|
+
? error.runnerCode
|
|
4447
|
+
: "unknown";
|
|
4448
|
+
process.stderr.write(`${code}: ${error.message}\n`);
|
|
4449
|
+
process.exitCode = 1;
|
|
4450
|
+
});
|
|
4451
|
+
}
|