@deksden-com/dd-flow-cli 0.9.0-beta.150 → 0.9.0-beta.152

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +17 -22
  3. package/dist/build-info.json +6 -6
  4. package/dist/harness-runtime/lib/agy-error-boundary.mjs +26 -5
  5. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +67 -0
  6. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +10 -14
  7. package/dist/harness-runtime/lib/dd-agy.mjs +3 -3
  8. package/dist/harness-runtime/lib/dd-droid.mjs +2 -2
  9. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +62 -10
  10. package/dist/harness-runtime/lib/dd-opencode.mjs +32 -14
  11. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
  12. package/dist/harness-runtime/lib/dd-zcode.mjs +37 -12
  13. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -1
  14. package/dist/harness-runtime/lib/delegation-instructions.mjs +13 -4
  15. package/dist/harness-runtime/lib/model-observations.mjs +19 -7
  16. package/dist/harness-runtime/lib/native-children.d.mts +12 -1
  17. package/dist/harness-runtime/lib/native-children.mjs +47 -1
  18. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +9 -1
  19. package/dist/harness-runtime/lib/provider-overload-policy.mjs +97 -6
  20. package/dist/services/check-runner-evidence.js +54 -309
  21. package/dist/services/check-runner-proof.js +373 -0
  22. package/dist/services/code-checks.js +31 -12
  23. package/dist/services/controller-child-capacity.js +58 -0
  24. package/dist/services/controller-child-observer.js +9 -3
  25. package/dist/services/memory-init-native-capacity.js +1 -1
  26. package/dist/services/provider-capacity.js +15 -7
  27. package/dist/services/repair-intents.js +4 -1
  28. package/dist/services/run-controller-adapter.js +1 -1
  29. package/dist/services/run-controller.js +87 -18
  30. package/dist/services/run-recovery-guard.js +1 -1
  31. package/dist/services/run-recovery-runtime.js +1 -1
  32. package/dist/services/stage-context.js +56 -8
  33. package/dist/services/vnext-code-review.js +1 -1
  34. package/dist/services/vnext-code.js +3 -2
  35. package/dist/services/vnext-fanout.js +1 -1
  36. package/dist/services/vnext-merge.js +1 -1
  37. package/dist/services/work-registry.js +1 -1
  38. package/dist/verification-components.json +299 -0
  39. package/package.json +7 -6
  40. package/tools/verification-jobs.mjs +317 -0
@@ -0,0 +1,299 @@
1
+ {
2
+ "schema_id": "dd-flow/verification-components@1",
3
+ "scope": "technical-matrix-publication-not-native-execution",
4
+ "matrix": {
5
+ "roots": [
6
+ "services/verification-matrix.js",
7
+ "services/stage-report-renderer.js",
8
+ "services/accepted-intent.js",
9
+ "services/check-runner-evidence.js",
10
+ "services/check-runner-proof.js"
11
+ ],
12
+ "runner_proof_contract": "dd-flow/check-runner-proof@1",
13
+ "files": [
14
+ "cli/command-inputs.js",
15
+ "cli/command-metadata.js",
16
+ "cli/command-option-descriptions.js",
17
+ "cli/help.js",
18
+ "cli/input-preparation.js",
19
+ "cli/native-hook-preparation.js",
20
+ "domain/contracts.js",
21
+ "domain/diagnostic-aspects.js",
22
+ "domain/diagnostic-flows.js",
23
+ "domain/entity-ids.js",
24
+ "domain/flow-contract.js",
25
+ "domain/session-coverage.js",
26
+ "domain/stage-catalog.js",
27
+ "domain/stage-handler-identities.js",
28
+ "domain/validation.js",
29
+ "protocol/local-files.js",
30
+ "runtime/context.js",
31
+ "schemas/merge-result.schema.json",
32
+ "schemas/merge-stage-report-legacy-0.4.2.schema.json",
33
+ "schemas/merge-stage-report.schema.json",
34
+ "schemas/plan-aspect-map.schema.json",
35
+ "schemas/plan-review-decision.schema.json",
36
+ "schemas/plan-review-result.schema.json",
37
+ "schemas/plan-stage-report.schema.json",
38
+ "schemas/protocol-dashboard-data.schema.json",
39
+ "schemas/protocol-plan.schema.json",
40
+ "schemas/run-control-receipt.schema.json",
41
+ "schemas/run-control-request.schema.json",
42
+ "schemas/run-engine-binding.schema.json",
43
+ "schemas/stage-finish-input.schema.json",
44
+ "schemas/stage-prompt.schema.json",
45
+ "schemas/stage-report.schema.json",
46
+ "schemas/stage-start-response.schema.json",
47
+ "services/accepted-intent.js",
48
+ "services/audit.js",
49
+ "services/branch-context.js",
50
+ "services/build-info.js",
51
+ "services/canon.js",
52
+ "services/check-environment.js",
53
+ "services/check-runner-evidence.js",
54
+ "services/check-runner-proof.js",
55
+ "services/cli-operation-classifier.js",
56
+ "services/cli-upgrade-allowlist.js",
57
+ "services/code-checks.js",
58
+ "services/command-context.js",
59
+ "services/config.js",
60
+ "services/descriptor-coordinator-session.js",
61
+ "services/descriptor-root-work.js",
62
+ "services/descriptor-run-settlement.js",
63
+ "services/descriptor-run-start.js",
64
+ "services/descriptor-stage-coordinator.js",
65
+ "services/descriptor-stage-settlement.js",
66
+ "services/descriptor-stage-start.js",
67
+ "services/diagnostic-evidence.js",
68
+ "services/diagnostic-report-publication.js",
69
+ "services/diagnostic-report.js",
70
+ "services/diagnostic-snapshot.js",
71
+ "services/diagnostic-stage-handlers.js",
72
+ "services/diagnostic-stage-results.js",
73
+ "services/diagnostic-work-batch.js",
74
+ "services/diagnostic-work-collection.js",
75
+ "services/diagnostic-work-registration.js",
76
+ "services/diagnostic-work-results.js",
77
+ "services/engines.js",
78
+ "services/eval-snapshots.js",
79
+ "services/execution-bundles.js",
80
+ "services/execution-policy.js",
81
+ "services/executor-onboarding.js",
82
+ "services/external-work-launch.js",
83
+ "services/flow-descriptors.js",
84
+ "services/flow-guidance.js",
85
+ "services/frozen-engine.js",
86
+ "services/generic-execution.js",
87
+ "services/hook-memory-routing.js",
88
+ "services/hook-recovery-guard.js",
89
+ "services/hook-session-observation.js",
90
+ "services/hooks.js",
91
+ "services/hybrid-contracts.js",
92
+ "services/hybrid-digest.js",
93
+ "services/ids.js",
94
+ "services/installation-lock.js",
95
+ "services/installed-flow-source.js",
96
+ "services/lanes.js",
97
+ "services/lifecycle-command.js",
98
+ "services/lifecycle-contract.js",
99
+ "services/lifecycle-invocations.js",
100
+ "services/managed-daemon-binding.js",
101
+ "services/managed-processes.js",
102
+ "services/memory-accepted-composition.js",
103
+ "services/memory-baseline-preservation.js",
104
+ "services/memory-completed-correction-wave.js",
105
+ "services/memory-compose-handler.js",
106
+ "services/memory-composition-acceptance.js",
107
+ "services/memory-composition-graph.js",
108
+ "services/memory-composition-registration.js",
109
+ "services/memory-composition-work-custody.js",
110
+ "services/memory-composition-work-results.js",
111
+ "services/memory-corrected-review-acceptance.js",
112
+ "services/memory-corrected-review-batch.js",
113
+ "services/memory-corrected-review-findings.js",
114
+ "services/memory-corrected-review-inputs.js",
115
+ "services/memory-corrected-review-projection.js",
116
+ "services/memory-corrected-review-receipt.js",
117
+ "services/memory-corrected-review-registration.js",
118
+ "services/memory-correction-check-receipt.js",
119
+ "services/memory-correction-input-owner.js",
120
+ "services/memory-custody-snapshot.js",
121
+ "services/memory-discover-handler.js",
122
+ "services/memory-finish-evidence.js",
123
+ "services/memory-finish-handler.js",
124
+ "services/memory-finish-integration.js",
125
+ "services/memory-finish-readiness.js",
126
+ "services/memory-git-integration.js",
127
+ "services/memory-historical-corrected-review.js",
128
+ "services/memory-historical-original-review.js",
129
+ "services/memory-historical-repair-registration.js",
130
+ "services/memory-historical-second-review.js",
131
+ "services/memory-init-bootstrap-readiness.js",
132
+ "services/memory-init-engine-release.js",
133
+ "services/memory-init-git-policy.js",
134
+ "services/memory-init-lint-capability.js",
135
+ "services/memory-init-preflight.js",
136
+ "services/memory-init-project-bootstrap.js",
137
+ "services/memory-init-request-candidate.js",
138
+ "services/memory-init-request-input.js",
139
+ "services/memory-init-resource-storage.js",
140
+ "services/memory-init-run-allocation.js",
141
+ "services/memory-init-startup-custody.js",
142
+ "services/memory-init-storage.js",
143
+ "services/memory-init-worktrunk-capability.js",
144
+ "services/memory-layer-publication.js",
145
+ "services/memory-layer.js",
146
+ "services/memory-lint-config.js",
147
+ "services/memory-native-review-copy.js",
148
+ "services/memory-normal-flow-entry.js",
149
+ "services/memory-ordinary-defaults.js",
150
+ "services/memory-permissions.js",
151
+ "services/memory-preflight-bootstrap-custody.js",
152
+ "services/memory-preflight-handler.js",
153
+ "services/memory-repair-current-view.js",
154
+ "services/memory-repair-document-delta.js",
155
+ "services/memory-repair-history.js",
156
+ "services/memory-repair-physical-binding.js",
157
+ "services/memory-repair-publication-files.js",
158
+ "services/memory-repair-publication-intent.js",
159
+ "services/memory-repair-publication-plan.js",
160
+ "services/memory-repair-publication-source.js",
161
+ "services/memory-repair-publication.js",
162
+ "services/memory-repair-registration.js",
163
+ "services/memory-repair-work-baseline.js",
164
+ "services/memory-repair-work-completion.js",
165
+ "services/memory-repair-work-custody.js",
166
+ "services/memory-repair-work-results.js",
167
+ "services/memory-repair-workspace.js",
168
+ "services/memory-repair-writer-copy.js",
169
+ "services/memory-result-binding.js",
170
+ "services/memory-retained-review-authority.js",
171
+ "services/memory-retained-review-results.js",
172
+ "services/memory-retained-second-baseline.js",
173
+ "services/memory-retained-second-repairs.js",
174
+ "services/memory-review-acceptance.js",
175
+ "services/memory-review-correction-plan.js",
176
+ "services/memory-review-handler.js",
177
+ "services/memory-review-inputs.js",
178
+ "services/memory-review-limits.js",
179
+ "services/memory-review-receipt.js",
180
+ "services/memory-review-registration.js",
181
+ "services/memory-review-work-custody.js",
182
+ "services/memory-review-work-input.js",
183
+ "services/memory-review-work-results.js",
184
+ "services/memory-second-corrected-review-inputs.js",
185
+ "services/memory-second-correction-plan.js",
186
+ "services/memory-second-repair-baseline.js",
187
+ "services/memory-second-repair-launch.js",
188
+ "services/memory-second-repair-registration.js",
189
+ "services/memory-second-repair-source.js",
190
+ "services/memory-second-review-acceptance.js",
191
+ "services/memory-source-observations.js",
192
+ "services/memory-stage-dispatch.js",
193
+ "services/memory-stage-results.js",
194
+ "services/memory-tool-probe.js",
195
+ "services/memory-upgrade-analysis-handler.js",
196
+ "services/memory-upgrade-bootstrap-readiness.js",
197
+ "services/memory-upgrade-evidence.js",
198
+ "services/memory-upgrade-inventory.js",
199
+ "services/memory-upgrade-publication-custody.js",
200
+ "services/memory-upgrade-publication.js",
201
+ "services/memory-upgrade-selection.js",
202
+ "services/memory-upgrade-storage-readiness.js",
203
+ "services/memory-verification-acceptance.js",
204
+ "services/memory-verification-checks.js",
205
+ "services/memory-verification-receipt.js",
206
+ "services/memory-verify-handler.js",
207
+ "services/memory-work-result-json.js",
208
+ "services/memory-work-result-preparation.js",
209
+ "services/merge-queue.js",
210
+ "services/migrations.js",
211
+ "services/native-session-control.js",
212
+ "services/operator-profiles.js",
213
+ "services/plan-runtime.js",
214
+ "services/portable-refs.js",
215
+ "services/project-registry.js",
216
+ "services/project-summary.js",
217
+ "services/projects.js",
218
+ "services/prompt-delivery.js",
219
+ "services/protocol-lifecycle.js",
220
+ "services/protocols.js",
221
+ "services/provider-capacity.js",
222
+ "services/recovery-snapshot-database.js",
223
+ "services/release-archive.js",
224
+ "services/release-cache.js",
225
+ "services/release-download.js",
226
+ "services/release-envelope.js",
227
+ "services/release-manifest.js",
228
+ "services/release-payload.js",
229
+ "services/release-source.js",
230
+ "services/repair-intents.js",
231
+ "services/repair-receipt.js",
232
+ "services/review-copy.js",
233
+ "services/review-deferral.js",
234
+ "services/run-control-receipt.js",
235
+ "services/run-control.js",
236
+ "services/run-controller-state.js",
237
+ "services/run-engine-bindings.js",
238
+ "services/run-observations.js",
239
+ "services/run-projection.js",
240
+ "services/run-recovery-guard.js",
241
+ "services/run-recovery.js",
242
+ "services/run-start-custody.js",
243
+ "services/runs.js",
244
+ "services/runtime-budget.js",
245
+ "services/runtime-command.js",
246
+ "services/schema-validation.js",
247
+ "services/scope-writer-cleanup.js",
248
+ "services/session-identity.js",
249
+ "services/sessions.js",
250
+ "services/setup.js",
251
+ "services/stage-context.js",
252
+ "services/stage-handlers.js",
253
+ "services/stage-lifecycle.js",
254
+ "services/stage-pause.js",
255
+ "services/stage-report-renderer.js",
256
+ "services/stage-work-graph.js",
257
+ "services/tooling.js",
258
+ "services/usage.js",
259
+ "services/verification-matrix.js",
260
+ "services/version-status.js",
261
+ "services/vnext-code-review.js",
262
+ "services/vnext-code.js",
263
+ "services/vnext-contracts.js",
264
+ "services/vnext-execution-profile.js",
265
+ "services/vnext-fanout.js",
266
+ "services/vnext-merge.js",
267
+ "services/vnext-plan-review.js",
268
+ "services/vnext-plan.js",
269
+ "services/vnext-protocolize.js",
270
+ "services/vnext-specify.js",
271
+ "services/vnext-workspace-policy.js",
272
+ "services/work-registry.js",
273
+ "services/workspace-bootstrap.js",
274
+ "services/workspace-files.js",
275
+ "services/worktrees.js",
276
+ "shared/entity-references.js",
277
+ "shared/errors.js",
278
+ "shared/json.js",
279
+ "storage/agent-output.js",
280
+ "storage/database.js",
281
+ "storage/foreign-key-transaction.js",
282
+ "storage/immutable-input.js",
283
+ "storage/import-guard.js",
284
+ "storage/native-cleanup.js",
285
+ "storage/paths.js",
286
+ "storage/projection-lock.js",
287
+ "storage/work-references.js",
288
+ "storage/writer-contract.js"
289
+ ],
290
+ "dependencies": [
291
+ "ajv/dist/2020.js",
292
+ "ajv/dist/ajv.js",
293
+ "semver",
294
+ "shell-quote",
295
+ "smol-toml",
296
+ "yaml"
297
+ ]
298
+ }
299
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.150",
3
+ "version": "0.9.0-beta.152",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.23.0",
@@ -24,12 +24,13 @@
24
24
  "pnpm": ">=10.0.0"
25
25
  },
26
26
  "scripts": {
27
- "build": "tsc -p tsconfig.build.json && node scripts/generate-build-info.mjs && node scripts/copy-assets.mjs",
27
+ "build": "tsc -p tsconfig.build.json && node scripts/generate-build-info.mjs && node scripts/copy-assets.mjs && node scripts/generate-verification-components.mjs",
28
28
  "typecheck": "tsc --noEmit",
29
29
  "lint": "eslint . --max-warnings=0",
30
- "test": "pnpm gate:release",
31
- "gate:merge": "pnpm typecheck && pnpm lint && pnpm test:fast && pnpm test:hybrid",
32
- "gate:release": "node scripts/verify-test-inventory.mjs --profile-start && node scripts/run-test-receipt.mjs hybrid && node scripts/run-test-receipt.mjs canon && node scripts/run-test-receipt.mjs e2 && node scripts/run-test-receipt.mjs e3 && node scripts/run-test-receipt.mjs e4 && node scripts/run-test-receipt.mjs build && node scripts/run-test-receipt.mjs release && node scripts/run-test-receipt.mjs integration && node scripts/run-test-receipt.mjs runtime-sensitive && node scripts/verify-test-inventory.mjs --profile-finish",
30
+ "test": "node scripts/verification.mjs run",
31
+ "gate:merge": "node scripts/verification.mjs run",
32
+ "verify:plan": "node scripts/verification.mjs plan",
33
+ "test:verification": "node --test --test-concurrency=1 scripts/verification-policy.test.mjs scripts/generate-verification-components.test.mjs scripts/verification-jobs.test.mjs",
33
34
  "test:hybrid": "node --import ./scripts/source-test-loader.mjs --test --test-concurrency=1 scripts/hybrid-contracts.test.mjs",
34
35
  "test:hybrid:canon": "node --import ./scripts/source-test-loader.mjs --test --test-concurrency=1 scripts/hybrid-canon.test.mjs scripts/hybrid-inventory.test.mjs scripts/hybrid-consumers.test.mjs scripts/hybrid-schema-copies.test.mjs scripts/verify-e3-canon.test.mjs",
35
36
  "test:e2": "node --import ./scripts/source-test-loader.mjs --test --test-concurrency=1 scripts/e2-*.test.mjs",
@@ -38,7 +39,7 @@
38
39
  "test:e4": "node --import ./scripts/source-test-loader.mjs --test --test-concurrency=1 scripts/e4-*.test.mjs",
39
40
  "test:e3:canon": "node --import ./scripts/source-test-loader.mjs --test scripts/verify-e3-canon.test.mjs",
40
41
  "test:release": "node --test scripts/verify-release-build.test.mjs && vitest run --project=release",
41
- "test:release:build": "node --test --test-concurrency=1 scripts/verify-release-build.test.mjs",
42
+ "test:release:build": "node --test --test-concurrency=1 scripts/verify-release-build.test.mjs scripts/verify-e2-built-cli.test.mjs scripts/verify-e2-installed-cli.test.mjs",
42
43
  "test:integration": "vitest run --project='integration-*'",
43
44
  "test:fast": "vitest run --project=integration-fast",
44
45
  "test:core": "vitest run --project='integration-core-*'",
@@ -0,0 +1,317 @@
1
+ // SSOT for the cooperating verification entrypoints. Standalone consumers vendor
2
+ // this file byte-for-byte; contract catalogs and report validators stay local.
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { randomUUID } from 'node:crypto';
7
+ import { spawn, execFileSync } from 'node:child_process';
8
+ import { setTimeout as delay } from 'node:timers/promises';
9
+ import { setTimeout, clearTimeout } from 'node:timers';
10
+ import process from 'node:process';
11
+
12
+ const realHome = os.userInfo().homedir; // OS identity, never a fixture's HOME.
13
+ const providerCredentials = ['OPENAI_API_KEY','OPENROUTER_API_KEY','OPENAI_DECISIONS_API_KEY','ANTHROPIC_API_KEY','GEMINI_API_KEY','GOOGLE_API_KEY','XAI_API_KEY','ZAI_API_KEY','NODE_AUTH_TOKEN','NPM_TOKEN','NPM_ACCESS_TOKEN'];
14
+ const read = file => JSON.parse(fs.readFileSync(file, 'utf8'));
15
+ const optional = file => { try { return read(file); } catch (e) { if (e.code === 'ENOENT') return null; throw e; } };
16
+ const blocked = message => Object.assign(new Error(message), { code: 'verification_ownership_unknown' });
17
+ export function atomicJson(file, value) {
18
+ fs.mkdirSync(path.dirname(file), { recursive: true });
19
+ const temporary = `${file}.${randomUUID()}.tmp`;
20
+ try { fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' }); fs.renameSync(temporary, file); }
21
+ catch (error) {
22
+ try { fs.unlinkSync(temporary); } catch (cleanup) { if (cleanup.code !== 'ENOENT') error.cleanup_error = cleanup; }
23
+ throw error;
24
+ }
25
+ }
26
+
27
+ // Same physical identity fields as EVAL's process-snapshot helper. Observation
28
+ // failure is never interpreted as an empty process table.
29
+ function snapshot() {
30
+ return execFileSync('ps', ['-axo', 'pid=,ppid=,pgid=,lstart=,stat='], {
31
+ encoding: 'utf8', timeout: 5000, env: { PATH: process.env.PATH, LC_ALL: 'C', TZ: 'UTC' },
32
+ }).trim().split('\n').flatMap(line => {
33
+ const p = line.trim().split(/\s+/);
34
+ return p.length >= 9 ? [{ pid: +p[0], ppid: +p[1], pgid: +p[2], birth: p.slice(3, 8).join(' '), zombie: p[8].startsWith('Z') }] : [];
35
+ });
36
+ }
37
+ const same = (a, b) => a && b && a.pid === b.pid && a.birth === b.birth && !b.zombie;
38
+ function self() {
39
+ const identity = snapshot().find(p => p.pid === process.pid && !p.zombie);
40
+ if (!identity) throw blocked('Cannot establish verification owner identity');
41
+ return identity;
42
+ }
43
+ function alive(owner, processes = snapshot()) { return same(owner, processes.find(p => p.pid === owner?.pid)); }
44
+ function release(lock, owner) {
45
+ // Only one reclaimer can remove this unique fence. A late reclaimer cannot
46
+ // unlink the next owner's record after the directory has been reacquired.
47
+ fs.unlinkSync(path.join(lock, `fence-${owner.token}`));
48
+ fs.unlinkSync(path.join(lock, 'owner.json')); fs.rmdirSync(lock);
49
+ }
50
+ async function claim(lock, signal) {
51
+ fs.mkdirSync(path.dirname(lock), { recursive: true });
52
+ const identity = self();
53
+ for (;;) {
54
+ signal?.throwIfAborted();
55
+ try {
56
+ fs.mkdirSync(lock);
57
+ const owner = { ...identity, token: randomUUID(), groups: [] };
58
+ fs.writeFileSync(path.join(lock, `fence-${owner.token}`), '', { flag: 'wx' });
59
+ atomicJson(path.join(lock, 'owner.json'), owner);
60
+ return owner;
61
+ } catch (e) { if (e.code !== 'EEXIST') throw e; }
62
+ const owner = optional(path.join(lock, 'owner.json'));
63
+ if (owner) {
64
+ const processes = snapshot();
65
+ if (!alive(owner, processes)) {
66
+ // A dead coordinator is NOT cleanup. A surviving process group keeps
67
+ // the whole lease until its physical absence is positively observed.
68
+ if (!Array.isArray(owner.groups)) throw blocked(`Unconfirmed lease groups: ${lock}`);
69
+ if (!owner.groups.some(g => !g.pgid || processes.some(p => p.pgid === g.pgid && !p.zombie))) {
70
+ // Recheck the immutable owner token before removing its record.
71
+ if (optional(path.join(lock, 'owner.json'))?.token === owner.token) {
72
+ try { release(lock, owner); } catch (e) { if (!['ENOENT', 'ENOTEMPTY'].includes(e.code)) throw e; }
73
+ }
74
+ continue;
75
+ }
76
+ }
77
+ }
78
+ await delay(50, undefined, { signal });
79
+ }
80
+ }
81
+
82
+ /** One immutable producer attempt. The index is scheduling state, not proof;
83
+ * callers must validate the returned report bytes and meaningful inputs. */
84
+ export async function withContractAttempt(index, { rerun = false, reason, signal, validate, validationKey } = {}, action) {
85
+ if (rerun && !reason) throw new Error('Explicit rerun requires a reason');
86
+ const lock = `${index}.claim`;
87
+ const initial = optional(index);
88
+ if (initial?.status === 'running' && !rerun) {
89
+ const result = await joinAttempt(initial, signal);
90
+ await validate?.(result.result, initial);
91
+ signal?.throwIfAborted();
92
+ return result;
93
+ }
94
+ let joined = null;
95
+ const owner = await claim(lock, signal);
96
+ let attempt;
97
+ try {
98
+ const current = optional(index); // Mandatory reread under claim.
99
+ if (current?.status === 'running' && !rerun) { joined = current; }
100
+ else if (current && !rerun) {
101
+ if (current.status !== 'passed') throw blocked(`Retained ${current.status} attempt requires explicit rerun: ${index}`);
102
+ try {
103
+ const terminal = read(current.report);
104
+ if (terminal.attemptId !== current.attemptId || terminal.status !== 'passed') throw new Error('Invalid terminal attempt');
105
+ await validate?.(terminal.result, current);
106
+ signal?.throwIfAborted();
107
+ return { ...terminal, reused: true, joined: false };
108
+ } catch (error) {
109
+ signal?.throwIfAborted(); // A cancelled reader never invalidates its producer.
110
+ atomicJson(index, { ...current, status: 'evidence_invalid', reason: String(error.message) });
111
+ reason = `Invalid evidence: ${error.message}`;
112
+ }
113
+ }
114
+ if (!joined) {
115
+ if (current?.status === 'running') throw blocked('Cannot supersede an active producer');
116
+ const attemptId = randomUUID();
117
+ const root = path.join(`${index}.attempts`, attemptId);
118
+ fs.mkdirSync(root, { recursive: true });
119
+ attempt = { attemptId, root, report: path.join(root, 'terminal.json'), owner, status: 'running', reason: reason ?? null, validationKey };
120
+ // Accepted rerun supersedes an old PASS before dispatch.
121
+ atomicJson(index, attempt);
122
+ }
123
+ } finally { release(lock, owner); }
124
+ if (joined) {
125
+ const result = await joinAttempt(joined, signal);
126
+ await validate?.(result.result, joined);
127
+ signal?.throwIfAborted();
128
+ return result;
129
+ }
130
+ try {
131
+ signal?.throwIfAborted();
132
+ const result = await action({ attemptId: attempt.attemptId, root: attempt.root });
133
+ await validate?.(result, attempt);
134
+ signal?.throwIfAborted();
135
+ const terminal = { attemptId: attempt.attemptId, result, status: 'passed' };
136
+ atomicJson(attempt.report, terminal);
137
+ atomicJson(index, { ...attempt, status: 'passed' });
138
+ return { ...terminal, reused: false, joined: false };
139
+ } catch (error) {
140
+ const terminal = { attemptId: attempt.attemptId, status: signal?.aborted ? 'interrupted' : 'failed', error: String(error.stack ?? error) };
141
+ atomicJson(attempt.report, terminal);
142
+ atomicJson(index, { ...attempt, status: terminal.status });
143
+ throw error;
144
+ }
145
+ }
146
+ async function joinAttempt(attempt, signal) {
147
+ for (;;) {
148
+ signal?.throwIfAborted(); // Cancels only this waiter.
149
+ const terminal = optional(attempt.report);
150
+ if (terminal) {
151
+ if (terminal.attemptId !== attempt.attemptId || terminal.status !== 'passed') throw blocked(`Producer attempt did not pass: ${attempt.attemptId}`);
152
+ return { ...terminal, reused: false, joined: true };
153
+ }
154
+ if (!alive(attempt.owner)) throw blocked(`Producer died without terminal evidence: ${attempt.attemptId}`);
155
+ await delay(50, undefined, { signal });
156
+ }
157
+ }
158
+
159
+ function isAncestor(identity, processes) {
160
+ let cursor = processes.find(p => p.pid === process.pid);
161
+ const seen = new Set();
162
+ while (cursor && !seen.has(cursor.pid)) {
163
+ if (same(identity, cursor)) return true;
164
+ seen.add(cursor.pid); cursor = processes.find(p => p.pid === cursor.ppid);
165
+ }
166
+ return false;
167
+ }
168
+ async function inheritedSlot(env, home, signal) {
169
+ if (!env.DD_FLOW_VERIFICATION_SLOT) return false;
170
+ let context;
171
+ try { context = JSON.parse(env.DD_FLOW_VERIFICATION_SLOT); } catch { throw blocked('Malformed inherited slot'); }
172
+ for (;;) {
173
+ const owner = optional(path.join(home, 'lease', 'owner.json')), processes = snapshot();
174
+ const group = owner?.groups?.find(g => g.token === context.token);
175
+ if (!group || owner.token !== context.owner || !alive(owner, processes)) break;
176
+ // A child may reach its first import before spawn() publishes its identity.
177
+ if (!group.pid) { await delay(10, undefined, { signal }); continue; }
178
+ if (isAncestor(group, processes)) return { owner, group };
179
+ break;
180
+ }
181
+ throw blocked('Inherited slot is not owned by an ancestor job');
182
+ }
183
+ async function nestedScope(env, home, outer, signal) {
184
+ if (!env.DD_FLOW_VERIFICATION_NESTED_SLOT) return outer ? `${outer.owner.token}-${outer.group.token}` : null;
185
+ let context;
186
+ try { context = JSON.parse(env.DD_FLOW_VERIFICATION_NESTED_SLOT); } catch { throw blocked('Malformed nested slot'); }
187
+ if (!outer || !/^[a-f0-9-]+$/.test(context.scope ?? '')) throw blocked('Invalid nested slot scope');
188
+ for (;;) {
189
+ const owner = optional(path.join(home, 'nested-slots', context.scope, 'owner.json')), processes = snapshot();
190
+ const child = owner?.groups?.find(group => group.token === context.child);
191
+ if (!child || owner.token !== context.owner || !alive(owner, processes)) break;
192
+ if (!child.pid) { await delay(10, undefined, { signal }); continue; }
193
+ if (isAncestor(child, processes)) return `${owner.token}-${child.token}`;
194
+ break;
195
+ }
196
+ throw blocked('Nested slot is not owned by an ancestor dispatch');
197
+ }
198
+
199
+ /** FIFO host-wide pool; descriptors are child commands, never callbacks sharing
200
+ * process.env. Exclusive drains both slots; inherited runners use one slot. */
201
+ export async function runJobs(jobs, { jobs: budget = 2, signal, env = process.env, coordinationHome } = {}) {
202
+ const queuedAt = Date.now();
203
+ if (![1, 2].includes(budget)) throw new Error('Verification budget must be 1 or 2');
204
+ if (Array.isArray(jobs) && jobs.some(job => job?.credentialKeys !== undefined && (!Array.isArray(job.credentialKeys) || job.credentialKeys.some(name => typeof name !== 'string' || !providerCredentials.includes(name))))) throw new Error('Invalid credential allowlist');
205
+ if (!Array.isArray(jobs) || jobs.some(job=>!job || typeof job.id!=='string' || !job.id || typeof job.command!=='string' || !job.command || job.args && (!Array.isArray(job.args)||job.args.some(arg=>typeof arg!=='string')) || job.executionClass!==undefined && !['isolated','exclusive'].includes(job.executionClass)) || new Set(jobs.map(job=>job.id)).size!==jobs.length) throw new Error('Invalid/duplicate verification job descriptor');
206
+ const home = coordinationHome ?? env.DD_FLOW_VERIFICATION_COORDINATION_HOME ?? path.join(realHome, '.dd-flow', 'verification-coordinator');
207
+ const nested = await inheritedSlot(env, home, signal);
208
+ const scope = await nestedScope(env, home, nested, signal);
209
+ const lock = scope ? path.join(home, 'nested-slots', scope) : path.join(home, 'lease');
210
+ const owner = await claim(lock, signal);
211
+ // A dead nested coordinator cannot free this slot while its outer group lives.
212
+ if (nested) { owner.groups = [{ ...nested.group, token: 'outer-group' }]; atomicJson(path.join(lock, 'owner.json'), owner); }
213
+ const results = new Array(jobs.length);
214
+ const active = new Set();
215
+ let failure;
216
+ const save = () => { if (owner) atomicJson(path.join(lock, 'owner.json'), owner); };
217
+ const start = (job, index) => {
218
+ signal?.throwIfAborted();
219
+ if (failure) throw failure;
220
+ const promise = execute(job, { home, owner, nested, scope, signal, env, save, queuedAt }).then(result => { results[index] = result; }, error => { failure ??= error; }).finally(() => active.delete(promise));
221
+ active.add(promise); return promise;
222
+ };
223
+ try {
224
+ for (let i = 0; i < jobs.length; i++) {
225
+ signal?.throwIfAborted();
226
+ if (failure) throw failure;
227
+ if (jobs[i].executionClass === 'exclusive') { await Promise.all(active); await start(jobs[i], i); }
228
+ else { while (active.size >= (nested ? 1 : budget)) await Promise.race(active); start(jobs[i], i); }
229
+ }
230
+ await Promise.all(active);
231
+ if (failure) throw failure;
232
+ return results;
233
+ } finally {
234
+ await Promise.allSettled(active);
235
+ // Never release a lease after unknown cleanup.
236
+ if (nested && owner.groups.every(group => group.token === 'outer-group')) owner.groups = [];
237
+ if (owner.groups.length === 0) release(lock, owner);
238
+ }
239
+ }
240
+
241
+ async function execute(job, { home, owner, nested, scope, signal, env, save, queuedAt }) {
242
+ const started = Date.now();
243
+ const attemptId = randomUUID();
244
+ const root = path.join(home, 'jobs', attemptId);
245
+ fs.mkdirSync(root, { recursive: true });
246
+ const childEnv = { ...env, ...job.env };
247
+ for (const name of providerCredentials) if (!job.credentialKeys?.includes(name)) delete childEnv[name];
248
+ childEnv.DD_FLOW_VERIFICATION_COORDINATION_HOME = home;
249
+ // node:test sets this private flag for its own children; a separately managed
250
+ // test command must produce a standalone report, not use its parent's IPC.
251
+ delete childEnv.NODE_TEST_CONTEXT;
252
+ for (const name of ['HOME', 'TMPDIR', 'XDG_CACHE_HOME', 'XDG_CONFIG_HOME', 'XDG_RUNTIME_DIR', 'CODEX_HOME', 'AGY_HOME', 'DD_FLOW_HOME', 'DD_FLOW_RESOURCE_HOME', 'DD_FLOW_CONFIG_HOME']) {
253
+ childEnv[name] = path.join(root, name.toLowerCase()); fs.mkdirSync(childEnv[name], { recursive: true });
254
+ }
255
+ // Only an explicit native/delivery descriptor may opt into provider credentials.
256
+ const token = randomUUID();
257
+ if (!nested) {
258
+ childEnv.DD_FLOW_VERIFICATION_SLOT = JSON.stringify({ owner: owner.token, token });
259
+ delete childEnv.DD_FLOW_VERIFICATION_NESTED_SLOT;
260
+ } else {
261
+ childEnv.DD_FLOW_VERIFICATION_SLOT = env.DD_FLOW_VERIFICATION_SLOT;
262
+ childEnv.DD_FLOW_VERIFICATION_NESTED_SLOT = JSON.stringify({ scope, owner: owner.token, child: token });
263
+ }
264
+ // Unknown spawn outcome must retain the lease after coordinator death.
265
+ owner.groups.push({ token, attemptId, root, pgid: null }); save();
266
+ const child = spawn(job.command, job.args ?? [], { cwd: job.cwd, env: childEnv, stdio: job.stdio ?? 'inherit', detached: !nested });
267
+ let spawnError;
268
+ const closed = new Promise(resolve => child.once('close', resolve));
269
+ const completion = new Promise(resolve => { child.once('error', error => { spawnError = error; resolve({ code: null, signal: null }); }); child.once('exit', (code, exitSignal) => resolve({ code, signal: exitSignal })); });
270
+ // A pipe is owned by this helper, not exposed to callers. Drain it while
271
+ // preserving output; an orphan holding it open must not hide direct exit.
272
+ for (const [name, stream] of [['stdout', child.stdout], ['stderr', child.stderr]]) stream?.on('data', bytes => fs.appendFileSync(path.join(root, `${name}.log`), bytes));
273
+ let identity;
274
+ if (child.pid) identity = snapshot().find(p => p.pid === child.pid && !p.zombie);
275
+ if (identity && owner) { identity = { ...identity, token, attemptId, root }; owner.groups = owner.groups.map(g => g.token === token ? identity : g); save(); }
276
+ let cleanupError;
277
+ const stop = (killSignal = 'SIGTERM') => {
278
+ if (!identity) return;
279
+ try {
280
+ const current = snapshot().find(p => p.pid === identity.pid);
281
+ if (same(identity, current)) process.kill(nested ? identity.pid : -identity.pgid, killSignal);
282
+ } catch (error) { if (error.code !== 'ESRCH') cleanupError = error; }
283
+ };
284
+ let forceTimer;
285
+ const cancel = () => { stop(); forceTimer = setTimeout(() => stop('SIGKILL'), 1000); };
286
+ signal?.addEventListener('abort', cancel, { once: true });
287
+ if (signal?.aborted) cancel();
288
+ const result = await completion;
289
+ clearTimeout(forceTimer);
290
+ signal?.removeEventListener('abort', cancel);
291
+ try {
292
+ if (cleanupError) throw cleanupError;
293
+ if (identity && !nested) {
294
+ for (let n = 0; n < 100 && snapshot().some(p => p.pgid === identity.pgid && !p.zombie); n++) {
295
+ stop(); await delay(50);
296
+ }
297
+ if (snapshot().some(p => p.pgid === identity.pgid && !p.zombie)) throw blocked(`Owned group cleanup unconfirmed: ${identity.pgid}`);
298
+ }
299
+ if (child.pid && !identity && snapshot().some(p => !p.zombie && (nested ? p.pid === child.pid : p.pgid === child.pid))) throw blocked(`Spawned process identity unconfirmed: ${child.pid}`);
300
+ const ioClosed = await Promise.race([closed.then(() => true), delay(5000, undefined, { ref: false }).then(() => false)]);
301
+ if (!ioClosed) {
302
+ owner.groups = owner.groups.map(group => group.token === token ? { ...group, pgid: null, reason: 'stdio_cleanup_unconfirmed' } : group); save();
303
+ throw blocked(`Owned stdio cleanup unconfirmed: ${child.pid}`);
304
+ }
305
+ // Missing startup identity is not an eternal unknown after actual exit plus
306
+ // positive absence of the exact spawned group. Never kill by an inferred PID.
307
+ owner.groups = owner.groups.filter(g => g.token !== token); save();
308
+ if (spawnError) throw spawnError;
309
+ const finished = Date.now();
310
+ const terminal = { id: job.id, attemptId, root, ...result, started_at: new Date(started).toISOString(), finished_at: new Date(finished).toISOString(), queue_ms: started - queuedAt, elapsed_ms: finished - started, status: signal?.aborted ? 'interrupted' : result.code === 0 ? 'passed' : 'failed' };
311
+ atomicJson(path.join(root, 'terminal.json'), terminal);
312
+ return terminal;
313
+ } catch (error) {
314
+ child.stdout?.destroy(); child.stderr?.destroy();
315
+ throw error;
316
+ }
317
+ }