intentdna 1.9.0 → 1.9.3

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 (131) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +227 -87
  4. package/dist/cli/commands/run-lifecycle.d.ts +61 -11
  5. package/dist/cli/commands/run-lifecycle.js +184 -50
  6. package/dist/cli/commands/run-observer.d.ts +15 -0
  7. package/dist/cli/commands/run-observer.js +210 -0
  8. package/dist/cli/commands/run.d.ts +16 -20
  9. package/dist/cli/commands/run.js +461 -659
  10. package/dist/cli/commands/sync.js +16 -12
  11. package/dist/cli/index.js +14 -5
  12. package/dist/compiler/compile.d.ts +1 -0
  13. package/dist/compiler/compile.js +1 -1
  14. package/dist/compiler/controller.d.ts +16 -0
  15. package/dist/compiler/controller.js +693 -0
  16. package/dist/compiler/input-resolver.d.ts +2 -0
  17. package/dist/compiler/input-resolver.js +22 -6
  18. package/dist/hooks/cli.d.ts +45 -0
  19. package/dist/hooks/cli.js +263 -31
  20. package/dist/hooks/host-event.d.ts +57 -0
  21. package/dist/hooks/host-event.js +187 -0
  22. package/dist/hooks/index.d.ts +1 -0
  23. package/dist/hooks/index.js +1 -0
  24. package/dist/hooks/protocol.d.ts +7 -0
  25. package/dist/hooks/schema.d.ts +1 -0
  26. package/dist/hooks/schema.js +89 -1
  27. package/dist/hooks/state.d.ts +23 -1
  28. package/dist/hooks/state.js +9 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +1 -0
  31. package/dist/mcp/index.js +2 -0
  32. package/dist/mcp/tools-run.d.ts +4 -0
  33. package/dist/mcp/tools-run.js +419 -0
  34. package/dist/mcp/tools-state.d.ts +1 -1
  35. package/dist/mcp/tools-state.js +13 -241
  36. package/dist/runtime/artifact-store.d.ts +146 -0
  37. package/dist/runtime/artifact-store.js +1436 -0
  38. package/dist/runtime/canonical-attempt-outcome.d.ts +12 -0
  39. package/dist/runtime/canonical-attempt-outcome.js +195 -0
  40. package/dist/runtime/canonical-json.d.ts +10 -0
  41. package/dist/runtime/canonical-json.js +105 -0
  42. package/dist/runtime/canonical-run-application.d.ts +93 -0
  43. package/dist/runtime/canonical-run-application.js +229 -0
  44. package/dist/runtime/canonical-run-service.d.ts +454 -0
  45. package/dist/runtime/canonical-run-service.js +4760 -0
  46. package/dist/runtime/canonical-runtime-composition.d.ts +22 -0
  47. package/dist/runtime/canonical-runtime-composition.js +34 -0
  48. package/dist/runtime/canonical-runtime-projection.d.ts +3 -0
  49. package/dist/runtime/canonical-runtime-projection.js +4 -0
  50. package/dist/runtime/canonical-target-compiler.d.ts +46 -0
  51. package/dist/runtime/canonical-target-compiler.js +514 -0
  52. package/dist/runtime/claude-agent-identity.d.ts +2 -0
  53. package/dist/runtime/claude-agent-identity.js +21 -0
  54. package/dist/runtime/executable-run-plan.d.ts +328 -0
  55. package/dist/runtime/executable-run-plan.js +1485 -0
  56. package/dist/runtime/execution-authority.d.ts +83 -0
  57. package/dist/runtime/execution-authority.js +98 -0
  58. package/dist/runtime/handoff-resolver.js +4 -1
  59. package/dist/runtime/harness-pull-adapter.d.ts +146 -0
  60. package/dist/runtime/harness-pull-adapter.js +315 -0
  61. package/dist/runtime/index.d.ts +39 -14
  62. package/dist/runtime/index.js +29 -7
  63. package/dist/runtime/local-execution-authority.d.ts +194 -0
  64. package/dist/runtime/local-execution-authority.js +2897 -0
  65. package/dist/runtime/local-execution-reconciliation.d.ts +20 -0
  66. package/dist/runtime/local-execution-reconciliation.js +107 -0
  67. package/dist/runtime/local-execution-supervisor-script.d.ts +7 -0
  68. package/dist/runtime/local-execution-supervisor-script.js +793 -0
  69. package/dist/runtime/local-provider-sandbox.d.ts +42 -0
  70. package/dist/runtime/local-provider-sandbox.js +154 -0
  71. package/dist/runtime/local-verifier-execution.d.ts +24 -0
  72. package/dist/runtime/local-verifier-execution.js +290 -0
  73. package/dist/runtime/local-windows-execution-supervisor-script.d.ts +6 -0
  74. package/dist/runtime/local-windows-execution-supervisor-script.js +788 -0
  75. package/dist/runtime/plan-store.d.ts +30 -0
  76. package/dist/runtime/plan-store.js +231 -0
  77. package/dist/runtime/process-tree.d.ts +9 -0
  78. package/dist/runtime/process-tree.js +97 -24
  79. package/dist/runtime/providers/codex.js +2 -2
  80. package/dist/runtime/push-driver.d.ts +184 -0
  81. package/dist/runtime/push-driver.js +1002 -0
  82. package/dist/runtime/result-store.d.ts +34 -3
  83. package/dist/runtime/result-store.js +1073 -2
  84. package/dist/runtime/run-binding.d.ts +68 -0
  85. package/dist/runtime/run-binding.js +278 -0
  86. package/dist/runtime/run-contracts.d.ts +575 -0
  87. package/dist/runtime/run-contracts.js +58 -7
  88. package/dist/runtime/run-controller.d.ts +1 -6
  89. package/dist/runtime/run-controller.js +0 -41
  90. package/dist/runtime/run-store.d.ts +85 -7
  91. package/dist/runtime/run-store.js +2528 -186
  92. package/dist/runtime/skill-adapter.d.ts +18 -7
  93. package/dist/runtime/skill-adapter.js +100 -742
  94. package/dist/runtime/structured-output-validator.d.ts +44 -0
  95. package/dist/runtime/structured-output-validator.js +171 -0
  96. package/dist/runtime/verifier-command-binding.d.ts +22 -0
  97. package/dist/runtime/verifier-command-binding.js +162 -0
  98. package/dist/runtime/verifier.d.ts +46 -6
  99. package/dist/runtime/verifier.js +355 -53
  100. package/dist/runtime/windows-job-keeper.d.ts +47 -0
  101. package/dist/runtime/windows-job-keeper.js +231 -0
  102. package/dist/runtime/worker-executor.d.ts +17 -1
  103. package/dist/runtime/worker-executor.js +26 -5
  104. package/dist/runtime/workflow-plan-adapter.d.ts +19 -0
  105. package/dist/runtime/workflow-plan-adapter.js +502 -6
  106. package/dist/runtime/workflow-runtime-manifest.d.ts +1 -0
  107. package/dist/runtime/workflow-runtime-manifest.js +5 -0
  108. package/dist/runtime/workspace-isolation.d.ts +29 -2
  109. package/dist/runtime/workspace-isolation.js +488 -11
  110. package/dist/runtime/workspace-observation.d.ts +44 -0
  111. package/dist/runtime/workspace-observation.js +216 -0
  112. package/dist/schema/controller-registry.d.ts +0 -7
  113. package/dist/schema/controller-registry.js +0 -8
  114. package/dist/schema/types.d.ts +84 -10
  115. package/dist/schema/types.js +2 -0
  116. package/dist/schema/validate.js +151 -1
  117. package/dist/schema/validators/controllers.d.ts +1 -1
  118. package/dist/schema/validators/controllers.js +126 -41
  119. package/dist/schema/workflow-authoring-contract.js +2 -0
  120. package/dist/schema/yaml-parser.js +1 -1
  121. package/dist/templates/flutter-rewrite-new.dna.yaml +1 -0
  122. package/dist/templates/flutter-rewrite.dna.yaml +87 -60
  123. package/dist/templates/safe-refactoring.dna.yaml +1 -0
  124. package/native/windows-job-keeper/README.md +80 -0
  125. package/native/windows-job-keeper/bin/aarch64/intentdna-windows-job-keeper.exe +0 -0
  126. package/native/windows-job-keeper/bin/x86_64/intentdna-windows-job-keeper.exe +0 -0
  127. package/native/windows-job-keeper/keeper.c +979 -0
  128. package/native/windows-job-keeper/manifest.json +33 -0
  129. package/package.json +6 -2
  130. package/scripts/build-windows-job-keeper.mjs +172 -0
  131. package/scripts/verify-windows-job-keeper.mjs +184 -0
@@ -6,9 +6,12 @@
6
6
  * Team naming, CLI parsing, generated shell, and automatic rollback are excluded.
7
7
  */
8
8
  import { spawnSync } from "node:child_process";
9
- import { createHash } from "node:crypto";
9
+ import { createHash, randomUUID } from "node:crypto";
10
10
  import { existsSync, lstatSync, mkdirSync, realpathSync, } from "node:fs";
11
+ import { link, lstat, mkdir, readFile, rename, rm, stat, unlink, utimes, writeFile, } from "node:fs/promises";
12
+ import { tmpdir } from "node:os";
11
13
  import { basename, dirname, isAbsolute, join, relative, resolve, } from "node:path";
14
+ import { canonicalizeJson } from "./canonical-json.js";
12
15
  export class WorkspaceIsolationError extends Error {
13
16
  code;
14
17
  constructor(code, message) {
@@ -35,6 +38,17 @@ function git(cwd, args, allowFailure = false) {
35
38
  }
36
39
  return output;
37
40
  }
41
+ function sharedWorkspaceGitState(root) {
42
+ const topLevel = git(root, ["rev-parse", "--show-toplevel"], true);
43
+ const head = git(root, ["rev-parse", "--verify", "HEAD"], true);
44
+ if (topLevel.status !== 0 || head.status !== 0)
45
+ return null;
46
+ const branch = git(root, ["symbolic-ref", "--quiet", "HEAD"], true);
47
+ return {
48
+ head: head.stdout.trim(),
49
+ branch_ref: branch.status === 0 ? branch.stdout.trim() : null,
50
+ };
51
+ }
38
52
  function requireText(value, label) {
39
53
  const trimmed = value.trim();
40
54
  if (!trimmed || trimmed.includes("\0")) {
@@ -53,6 +67,63 @@ function canonicalExistingDirectory(path, label) {
53
67
  }
54
68
  return realpathSync(requested);
55
69
  }
70
+ function isContainedPath(root, candidate) {
71
+ const child = relative(root, candidate);
72
+ return child === "" || (!child.startsWith("..") && !isAbsolute(child));
73
+ }
74
+ /**
75
+ * Resolve an existing directory below a committed workspace without following
76
+ * symbolic-link components. This is suitable for verifier/process cwd values.
77
+ */
78
+ export async function canonicalWorkspaceDirectory(workspaceRoot, relativeDirectory, label = "working_directory") {
79
+ return resolveCanonicalWorkspaceDirectory(workspaceRoot, relativeDirectory, label, false);
80
+ }
81
+ async function resolveCanonicalWorkspaceDirectory(workspaceRoot, relativeDirectory, label, createMissing) {
82
+ const root = canonicalExistingDirectory(workspaceRoot, "workspace_root");
83
+ const requested = requireText(relativeDirectory, label);
84
+ if (isAbsolute(requested)) {
85
+ throw new WorkspaceIsolationError("invalid_request", `${label} must be relative`);
86
+ }
87
+ const candidate = resolve(root, requested);
88
+ if (!isContainedPath(root, candidate)) {
89
+ throw new WorkspaceIsolationError("workspace_mismatch", `${label} escapes the committed workspace`);
90
+ }
91
+ const child = relative(root, candidate);
92
+ let current = root;
93
+ for (const segment of child.split(/[\\/]+/).filter(Boolean)) {
94
+ current = join(current, segment);
95
+ let status;
96
+ try {
97
+ status = await lstat(current);
98
+ }
99
+ catch (error) {
100
+ if (error.code === "ENOENT" && createMissing) {
101
+ try {
102
+ await mkdir(current);
103
+ }
104
+ catch (mkdirError) {
105
+ if (mkdirError.code !== "EEXIST")
106
+ throw mkdirError;
107
+ }
108
+ status = await lstat(current);
109
+ }
110
+ else if (error.code === "ENOENT") {
111
+ throw new WorkspaceIsolationError("invalid_request", `${label} does not exist: ${candidate}`);
112
+ }
113
+ else {
114
+ throw error;
115
+ }
116
+ }
117
+ if (status.isSymbolicLink() || !status.isDirectory()) {
118
+ throw new WorkspaceIsolationError("workspace_mismatch", `${label} must contain only real directories: ${current}`);
119
+ }
120
+ }
121
+ const canonical = realpathSync(candidate);
122
+ if (!isContainedPath(root, canonical)) {
123
+ throw new WorkspaceIsolationError("workspace_mismatch", `${label} resolves outside the committed workspace`);
124
+ }
125
+ return canonical;
126
+ }
56
127
  function repositoryRoot(cwd) {
57
128
  const result = git(cwd, ["rev-parse", "--show-toplevel"], true);
58
129
  if (result.status !== 0 || !result.stdout.trim()) {
@@ -187,8 +258,9 @@ function expectedBranchRef(plan) {
187
258
  }
188
259
  function assertMatchingWorktree(plan, entry) {
189
260
  if (entry.branch_ref !== expectedBranchRef(plan)
261
+ || entry.head !== plan.base_commit
190
262
  || !sameGitRepository(plan.repository_root, plan.working_directory)) {
191
- throw new WorkspaceIsolationError("workspace_mismatch", `existing worktree is not owned by attempt ${plan.attempt_id}: ${plan.working_directory}`);
263
+ throw new WorkspaceIsolationError("workspace_conflict", `existing worktree is not owned by attempt ${plan.attempt_id}: ${plan.working_directory}`);
192
264
  }
193
265
  }
194
266
  export function planAttemptWorkspace(request) {
@@ -244,6 +316,9 @@ export function allocateAttemptWorkspace(plan) {
244
316
  if (existing) {
245
317
  assertMatchingWorktree(plan, existing);
246
318
  const state = workspaceState(plan.working_directory);
319
+ if (state.dirty || state.conflicted) {
320
+ throw new WorkspaceIsolationError("workspace_conflict", `existing pre-start worktree is ${state.conflicted ? "conflicted" : "dirty"}: ${plan.working_directory}`);
321
+ }
247
322
  return {
248
323
  ...plan,
249
324
  created: false,
@@ -261,16 +336,22 @@ export function allocateAttemptWorkspace(plan) {
261
336
  }
262
337
  const branchExists = git(plan.repository_root, ["show-ref", "--verify", "--quiet", expectedBranchRef(plan)], true).status === 0;
263
338
  if (branchExists) {
264
- throw new WorkspaceIsolationError("branch_conflict", `attempt branch already exists without its owned worktree: ${plan.branch_name}`);
339
+ const tip = resolveCommit(plan.repository_root, plan.branch_name);
340
+ if (tip !== plan.base_commit) {
341
+ throw new WorkspaceIsolationError("branch_conflict", `attempt branch exists at unexpected commit: ${plan.branch_name}`);
342
+ }
343
+ git(plan.repository_root, ["worktree", "add", plan.working_directory, plan.branch_name]);
344
+ }
345
+ else {
346
+ git(plan.repository_root, [
347
+ "worktree",
348
+ "add",
349
+ "-b",
350
+ plan.branch_name,
351
+ plan.working_directory,
352
+ plan.base_commit,
353
+ ]);
265
354
  }
266
- git(plan.repository_root, [
267
- "worktree",
268
- "add",
269
- "-b",
270
- plan.branch_name,
271
- plan.working_directory,
272
- plan.base_commit,
273
- ]);
274
355
  return {
275
356
  ...plan,
276
357
  created: true,
@@ -279,6 +360,402 @@ export function allocateAttemptWorkspace(plan) {
279
360
  conflicted_at_allocation: false,
280
361
  };
281
362
  }
363
+ function canonicalWorkspacePlan(payload) {
364
+ return {
365
+ workspace_plan_id: `sha256:${createHash("sha256").update(canonicalizeJson(payload)).digest("hex")}`,
366
+ payload,
367
+ };
368
+ }
369
+ function authorityKey(kind, path) {
370
+ return `${kind}:${createHash("sha256").update(realpathSync(path)).digest("hex")}`;
371
+ }
372
+ function committedSharedWorkspaceRoot(allocation) {
373
+ let candidate = resolve(allocation.working_directory);
374
+ for (;;) {
375
+ const lexicalKey = `none:${createHash("sha256").update(candidate).digest("hex")}`;
376
+ if (existsSync(candidate)
377
+ && (lexicalKey === allocation.authority_key || authorityKey("none", candidate) === allocation.authority_key)) {
378
+ return realpathSync(candidate);
379
+ }
380
+ const parent = dirname(candidate);
381
+ if (parent === candidate)
382
+ break;
383
+ candidate = parent;
384
+ }
385
+ throw new WorkspaceIsolationError("workspace_mismatch", "shared working_directory is no longer contained by its committed workspace root");
386
+ }
387
+ async function allocationFromPlan(plan) {
388
+ if (plan.payload.kind === "none") {
389
+ const root = committedSharedWorkspaceRoot(plan.payload);
390
+ const workingDirectory = await resolveCanonicalWorkspaceDirectory(root, relative(root, realpathSync(resolve(plan.payload.working_directory))) || ".", "working_directory", false);
391
+ const gitState = sharedWorkspaceGitState(root);
392
+ return {
393
+ ...plan.payload,
394
+ working_directory: workingDirectory,
395
+ workspace_plan_id: plan.workspace_plan_id,
396
+ observed_head: gitState?.head ?? null,
397
+ observed_branch_ref: gitState?.branch_ref ?? null,
398
+ };
399
+ }
400
+ const legacy = {
401
+ isolation: "worktree",
402
+ run_id: "workspace-authority",
403
+ step_id: "workspace-authority",
404
+ attempt_id: plan.payload.workspace_id,
405
+ repository_root: plan.payload.repository_root,
406
+ worktree_root: dirname(plan.payload.worktree_root),
407
+ working_directory: plan.payload.worktree_root,
408
+ branch_name: plan.payload.branch_name,
409
+ base_ref: plan.payload.base_ref,
410
+ base_commit: plan.payload.base_commit,
411
+ ownership_key: plan.payload.ownership_key,
412
+ };
413
+ allocateAttemptWorkspace(legacy);
414
+ const relativeWorkingDirectory = relative(plan.payload.worktree_root, plan.payload.working_directory);
415
+ if (!isContainedPath(resolve(plan.payload.worktree_root), resolve(plan.payload.working_directory))) {
416
+ throw new WorkspaceIsolationError("workspace_mismatch", "working_directory escapes the committed worktree");
417
+ }
418
+ const workingDirectory = await resolveCanonicalWorkspaceDirectory(plan.payload.worktree_root, relativeWorkingDirectory || ".", "working_directory", true);
419
+ const inspection = inspectAttemptWorkspace({
420
+ ...legacy,
421
+ created: false,
422
+ reused: true,
423
+ dirty_at_allocation: false,
424
+ conflicted_at_allocation: false,
425
+ });
426
+ if (!inspection.owned_by_attempt
427
+ || inspection.head !== plan.payload.base_commit
428
+ || inspection.branch_ref !== `refs/heads/${plan.payload.branch_name}`
429
+ || inspection.dirty
430
+ || inspection.conflicted) {
431
+ throw new WorkspaceIsolationError("workspace_conflict", "ensured worktree does not match the committed plan");
432
+ }
433
+ return {
434
+ ...plan.payload,
435
+ working_directory: workingDirectory,
436
+ workspace_plan_id: plan.workspace_plan_id,
437
+ observed_head: inspection.head,
438
+ observed_branch_ref: inspection.branch_ref,
439
+ };
440
+ }
441
+ function positiveInteger(value, label) {
442
+ if (!Number.isSafeInteger(value) || value <= 0) {
443
+ throw new WorkspaceIsolationError("invalid_request", `${label} must be a positive integer`);
444
+ }
445
+ return value;
446
+ }
447
+ function processIsAlive(pid) {
448
+ try {
449
+ process.kill(pid, 0);
450
+ return true;
451
+ }
452
+ catch (error) {
453
+ return error.code === "EPERM";
454
+ }
455
+ }
456
+ function parseLockOwner(text) {
457
+ try {
458
+ const value = JSON.parse(text);
459
+ return typeof value.owner === "string"
460
+ && typeof value.generation === "string"
461
+ && Number.isSafeInteger(value.pid)
462
+ && (value.pid ?? 0) > 0
463
+ && typeof value.acquired_at === "number"
464
+ ? value
465
+ : null;
466
+ }
467
+ catch {
468
+ return null;
469
+ }
470
+ }
471
+ export class LocalAttemptWorkspaceAuthority {
472
+ lockRoot;
473
+ lockWaitMs;
474
+ lockStaleMs;
475
+ lockRetryMs;
476
+ lockHeartbeatMs;
477
+ constructor(options = {}) {
478
+ this.lockRoot = resolve(options.lock_root ?? join(tmpdir(), "intentdna-workspace-authority-locks"));
479
+ this.lockWaitMs = positiveInteger(options.lock_wait_ms ?? 10_000, "lock_wait_ms");
480
+ this.lockStaleMs = positiveInteger(options.lock_stale_ms ?? 60_000, "lock_stale_ms");
481
+ this.lockRetryMs = positiveInteger(options.lock_retry_ms ?? 10, "lock_retry_ms");
482
+ this.lockHeartbeatMs = positiveInteger(options.lock_heartbeat_ms ?? 5_000, "lock_heartbeat_ms");
483
+ if (this.lockHeartbeatMs >= this.lockStaleMs) {
484
+ throw new WorkspaceIsolationError("invalid_request", "lock_heartbeat_ms must be less than lock_stale_ms");
485
+ }
486
+ }
487
+ async plan(request) {
488
+ const node = request.plan.plan.nodes.find((candidate) => candidate.node_id === request.activation.step_id);
489
+ if (!node || node.kind !== "worker") {
490
+ throw new WorkspaceIsolationError("invalid_request", "workspace planning requires a worker node");
491
+ }
492
+ const repositoryDirectory = request.run.run_binding.binding.workspace_root;
493
+ const legacy = planAttemptWorkspace({
494
+ isolation: node.workspace.isolation,
495
+ repository_directory: repositoryDirectory,
496
+ run_id: request.run.run_id,
497
+ step_id: request.activation.step_id,
498
+ attempt_id: request.attempt.attempt_id,
499
+ base_ref: "HEAD",
500
+ });
501
+ if (legacy.isolation === "none") {
502
+ const root = realpathSync(legacy.working_directory);
503
+ const workingDirectory = await resolveCanonicalWorkspaceDirectory(root, node.workspace.relative_working_directory, "relative_working_directory", true);
504
+ return canonicalWorkspacePlan({
505
+ kind: "none",
506
+ workspace_id: request.attempt.attempt_id,
507
+ working_directory: workingDirectory,
508
+ authority_key: authorityKey("none", root),
509
+ });
510
+ }
511
+ const workingDirectory = resolve(legacy.working_directory, node.workspace.relative_working_directory);
512
+ return canonicalWorkspacePlan({
513
+ kind: "worktree",
514
+ workspace_id: request.attempt.attempt_id,
515
+ working_directory: workingDirectory,
516
+ repository_root: legacy.repository_root,
517
+ worktree_root: legacy.working_directory,
518
+ branch_name: legacy.branch_name,
519
+ base_ref: legacy.base_ref,
520
+ base_commit: legacy.base_commit,
521
+ ownership_key: legacy.ownership_key,
522
+ authority_key: `worktree:${legacy.ownership_key}`,
523
+ });
524
+ }
525
+ async ensureWithExclusiveAuthority(plan, operation) {
526
+ return this.withLock(plan.payload.authority_key, async () => operation(await allocationFromPlan(plan)));
527
+ }
528
+ async withExclusiveMutationAuthority(request, operation) {
529
+ return this.withLock(request.allocation.authority_key, async () => {
530
+ await this.assertCommittedAllocation(request);
531
+ const fencingEpoch = await this.nextFencingEpoch(request.allocation.authority_key);
532
+ return operation({
533
+ workspace_id: request.allocation.workspace_id,
534
+ workspace_allocation_digest: `sha256:${createHash("sha256")
535
+ .update(canonicalizeJson(request.allocation), "utf8")
536
+ .digest("hex")}`,
537
+ authority_instance_id: `workspace-authority:${randomUUID()}`,
538
+ fencing_epoch: fencingEpoch,
539
+ });
540
+ });
541
+ }
542
+ async nextFencingEpoch(key) {
543
+ const digest = createHash("sha256").update(key).digest("hex");
544
+ const epochPath = join(this.lockRoot, `${digest}.epoch`);
545
+ let current = 0;
546
+ try {
547
+ const text = await readFile(epochPath, "utf8");
548
+ current = Number(text);
549
+ if (!Number.isSafeInteger(current) || current < 0) {
550
+ throw new WorkspaceIsolationError("workspace_conflict", `invalid workspace fencing epoch for ${key}`);
551
+ }
552
+ }
553
+ catch (error) {
554
+ if (error.code !== "ENOENT")
555
+ throw error;
556
+ }
557
+ const next = current + 1;
558
+ if (!Number.isSafeInteger(next)) {
559
+ throw new WorkspaceIsolationError("workspace_conflict", `workspace fencing epoch exhausted for ${key}`);
560
+ }
561
+ await writeFile(epochPath, String(next), "utf8");
562
+ return next;
563
+ }
564
+ async assertCommittedAllocation(request) {
565
+ const allocation = request.allocation;
566
+ if (allocation.kind === "none") {
567
+ const root = committedSharedWorkspaceRoot(allocation);
568
+ await resolveCanonicalWorkspaceDirectory(root, relative(root, realpathSync(resolve(allocation.working_directory))) || ".", "working_directory", false);
569
+ return;
570
+ }
571
+ const inspection = inspectAttemptWorkspace({
572
+ isolation: "worktree",
573
+ run_id: "workspace-authority",
574
+ step_id: "workspace-authority",
575
+ attempt_id: allocation.workspace_id,
576
+ repository_root: allocation.repository_root,
577
+ worktree_root: dirname(allocation.worktree_root),
578
+ working_directory: allocation.worktree_root,
579
+ branch_name: allocation.branch_name,
580
+ base_ref: allocation.base_ref,
581
+ base_commit: allocation.base_commit,
582
+ ownership_key: allocation.ownership_key,
583
+ created: false,
584
+ reused: true,
585
+ dirty_at_allocation: false,
586
+ conflicted_at_allocation: false,
587
+ });
588
+ if (!inspection.owned_by_attempt || inspection.branch_ref !== allocation.observed_branch_ref) {
589
+ throw new WorkspaceIsolationError("workspace_mismatch", "committed worktree identity changed");
590
+ }
591
+ if (request.require_clean_worktree && (inspection.dirty || inspection.conflicted)) {
592
+ throw new WorkspaceIsolationError("workspace_conflict", "committed worktree is not clean");
593
+ }
594
+ }
595
+ async withLock(key, operation) {
596
+ await mkdir(this.lockRoot, { recursive: true });
597
+ const digest = createHash("sha256").update(key).digest("hex");
598
+ const lockDirectory = join(this.lockRoot, `${digest}.lock`);
599
+ const owner = randomUUID();
600
+ const generation = randomUUID();
601
+ const ownerState = {
602
+ owner,
603
+ generation,
604
+ pid: process.pid,
605
+ acquired_at: Date.now(),
606
+ };
607
+ const ownerPath = join(lockDirectory, "owner.json");
608
+ const leasePath = join(lockDirectory, `lease-${generation}`);
609
+ const transitionPath = join(this.lockRoot, `${digest}.transition-${generation}`);
610
+ const deadline = Date.now() + this.lockWaitMs;
611
+ for (;;) {
612
+ try {
613
+ await mkdir(lockDirectory);
614
+ await writeFile(ownerPath, JSON.stringify(ownerState), { encoding: "utf8", flag: "wx" });
615
+ await writeFile(leasePath, "", { encoding: "utf8", flag: "wx" });
616
+ break;
617
+ }
618
+ catch (error) {
619
+ const code = error.code;
620
+ if (code !== "EEXIST") {
621
+ await rm(lockDirectory, { recursive: true, force: true });
622
+ throw error;
623
+ }
624
+ try {
625
+ const state = parseLockOwner(await readFile(ownerPath, "utf8"));
626
+ const currentLease = state
627
+ ? await stat(join(lockDirectory, `lease-${state.generation}`)).catch(() => null)
628
+ : null;
629
+ const heartbeatAt = currentLease?.mtimeMs ?? state?.acquired_at;
630
+ if (state
631
+ && heartbeatAt !== undefined
632
+ && Date.now() - heartbeatAt > this.lockStaleMs
633
+ && !processIsAlive(state.pid)) {
634
+ const observedTransition = join(this.lockRoot, `${digest}.transition-${state.generation}`);
635
+ try {
636
+ await link(ownerPath, observedTransition);
637
+ }
638
+ catch (transitionError) {
639
+ if (transitionError.code === "EEXIST") {
640
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, this.lockRetryMs));
641
+ continue;
642
+ }
643
+ throw transitionError;
644
+ }
645
+ const quarantine = `${lockDirectory}.stale-${randomUUID()}`;
646
+ try {
647
+ const claimed = parseLockOwner(await readFile(observedTransition, "utf8"));
648
+ if (claimed?.owner !== state.owner || claimed.generation !== state.generation) {
649
+ throw new WorkspaceIsolationError("workspace_conflict", `workspace authority changed during stale-lock takeover ${key}`);
650
+ }
651
+ const claimedLease = await stat(join(lockDirectory, `lease-${state.generation}`)).catch(() => null);
652
+ if (claimedLease
653
+ && Date.now() - claimedLease.mtimeMs <= this.lockStaleMs) {
654
+ continue;
655
+ }
656
+ await rename(lockDirectory, quarantine);
657
+ const moved = parseLockOwner(await readFile(join(quarantine, "owner.json"), "utf8").catch(() => ""));
658
+ if (moved?.owner !== state.owner || moved.generation !== state.generation) {
659
+ throw new WorkspaceIsolationError("workspace_conflict", `workspace authority changed during stale-lock takeover ${key}`);
660
+ }
661
+ await rm(quarantine, { recursive: true, force: true });
662
+ }
663
+ finally {
664
+ await unlink(observedTransition).catch(() => undefined);
665
+ }
666
+ continue;
667
+ }
668
+ }
669
+ catch (readError) {
670
+ if (readError.code === "ENOENT") {
671
+ // Another holder is publishing or replacing the lock. Retry; never
672
+ // delete a directory whose owner generation was not observed.
673
+ }
674
+ else if (readError instanceof WorkspaceIsolationError) {
675
+ throw readError;
676
+ }
677
+ }
678
+ if (Date.now() >= deadline) {
679
+ throw new WorkspaceIsolationError("workspace_conflict", `timed out acquiring workspace authority ${key}`);
680
+ }
681
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, this.lockRetryMs));
682
+ }
683
+ }
684
+ const heartbeat = setInterval(() => {
685
+ void (async () => {
686
+ try {
687
+ await link(ownerPath, transitionPath);
688
+ }
689
+ catch (error) {
690
+ if (["EEXIST", "ENOENT"].includes(error.code ?? ""))
691
+ return;
692
+ throw error;
693
+ }
694
+ try {
695
+ const claimed = parseLockOwner(await readFile(transitionPath, "utf8"));
696
+ const current = parseLockOwner(await readFile(ownerPath, "utf8"));
697
+ if (claimed?.owner === owner
698
+ && claimed.generation === generation
699
+ && current?.owner === owner
700
+ && current.generation === generation) {
701
+ const now = new Date();
702
+ await utimes(leasePath, now, now);
703
+ }
704
+ }
705
+ finally {
706
+ await unlink(transitionPath).catch(() => undefined);
707
+ }
708
+ })().catch(() => undefined);
709
+ }, this.lockHeartbeatMs);
710
+ heartbeat.unref();
711
+ try {
712
+ return await operation();
713
+ }
714
+ finally {
715
+ clearInterval(heartbeat);
716
+ try {
717
+ for (;;) {
718
+ const state = parseLockOwner(await readFile(ownerPath, "utf8"));
719
+ if (state?.owner !== owner || state.generation !== generation)
720
+ break;
721
+ try {
722
+ await link(ownerPath, transitionPath);
723
+ }
724
+ catch (error) {
725
+ if (error.code === "EEXIST") {
726
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, this.lockRetryMs));
727
+ continue;
728
+ }
729
+ if (error.code === "ENOENT") {
730
+ break;
731
+ }
732
+ else {
733
+ throw error;
734
+ }
735
+ }
736
+ try {
737
+ const claimed = parseLockOwner(await readFile(transitionPath, "utf8"));
738
+ const current = parseLockOwner(await readFile(ownerPath, "utf8"));
739
+ if (claimed?.owner === owner
740
+ && claimed.generation === generation
741
+ && current?.owner === owner
742
+ && current.generation === generation) {
743
+ await rm(lockDirectory, { recursive: true, force: true });
744
+ }
745
+ }
746
+ finally {
747
+ await unlink(transitionPath).catch(() => undefined);
748
+ }
749
+ break;
750
+ }
751
+ }
752
+ catch (error) {
753
+ if (error.code !== "ENOENT")
754
+ throw error;
755
+ }
756
+ }
757
+ }
758
+ }
282
759
  export function inspectAttemptWorkspace(workspace) {
283
760
  if (workspace.isolation === "none") {
284
761
  return {
@@ -0,0 +1,44 @@
1
+ export type WorkspaceObservationErrorCode = "source_drift" | "verification_failed" | "observation_limit_exceeded";
2
+ export declare class WorkspaceObservationError extends Error {
3
+ readonly code: WorkspaceObservationErrorCode;
4
+ constructor(code: WorkspaceObservationErrorCode, message: string, options?: ErrorOptions);
5
+ }
6
+ export interface WorkspaceObservationLimits {
7
+ readonly max_entries: number;
8
+ readonly max_bytes: number;
9
+ readonly max_depth: number;
10
+ readonly max_duration_ms: number;
11
+ }
12
+ export interface WorkspaceObservationHooks {
13
+ after_directory_read?(relativePath: string): Promise<void> | void;
14
+ }
15
+ export interface ObserveWorkspaceRequest {
16
+ readonly workspace_root: string;
17
+ readonly limits?: Partial<WorkspaceObservationLimits>;
18
+ readonly hooks?: WorkspaceObservationHooks;
19
+ readonly now_ms?: () => number;
20
+ }
21
+ export interface GitWorkspaceSnapshot {
22
+ readonly repository_identity_digest: `sha256:${string}`;
23
+ readonly head: string;
24
+ readonly symbolic_branch: string | null;
25
+ readonly porcelain_v2_digest: `sha256:${string}`;
26
+ readonly local_config_digest: `sha256:${string}`;
27
+ }
28
+ export interface WorkspaceObservation {
29
+ readonly schema_version: "intentdna.workspace_observation.v2";
30
+ readonly observed_digest: `sha256:${string}`;
31
+ readonly entry_count: number;
32
+ readonly byte_count: number;
33
+ readonly excluded_relative_paths: readonly [".dna/runtime", ".dna/worktrees/runtime"];
34
+ readonly git: GitWorkspaceSnapshot | null;
35
+ }
36
+ export declare const DEFAULT_WORKSPACE_OBSERVATION_LIMITS: WorkspaceObservationLimits;
37
+ export declare const WORKSPACE_OBSERVATION_EXCLUDED_PATHS: readonly [".dna/runtime", ".dna/worktrees/runtime"];
38
+ export declare function workspaceObservationPolicy(limits?: Partial<WorkspaceObservationLimits>): {
39
+ readonly schema_version: "intentdna.workspace_observation_policy.v1";
40
+ readonly limits: WorkspaceObservationLimits;
41
+ readonly excluded_relative_paths: typeof WORKSPACE_OBSERVATION_EXCLUDED_PATHS;
42
+ readonly git_observation: "head_branch_status_local_config";
43
+ };
44
+ export declare function observeWorkspace(request: ObserveWorkspaceRequest): Promise<WorkspaceObservation>;