@awebai/oats 0.25.9 → 0.27.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.
Files changed (183) hide show
  1. package/README.md +8 -6
  2. package/bin/oats.mjs +648 -1755
  3. package/capabilities/oats-authoring/oats-package.json +2 -2
  4. package/capabilities/oats-authoring/oats.json +2 -2
  5. package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +46 -25
  6. package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +13 -6
  7. package/capabilities/oats-aweb/bin/oats-aweb.mjs +279 -93
  8. package/capabilities/oats-aweb/injects/aweb.md +7 -2
  9. package/capabilities/oats-aweb/lib/binding-wire.mjs +89 -13
  10. package/capabilities/oats-aweb/lib/captured-native.mjs +1 -1
  11. package/capabilities/oats-aweb/lib/grant-custody.mjs +38 -0
  12. package/capabilities/oats-aweb/oats.json +8 -4
  13. package/capabilities/oats-jira/bin/oats-jira.mjs +4 -4
  14. package/capabilities/oats-jira/oats.json +2 -2
  15. package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +6 -3
  16. package/capabilities/oats-linear/bin/oats-linear-hook.mjs +6 -4
  17. package/capabilities/oats-linear/oats.json +2 -2
  18. package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +6 -0
  19. package/capabilities/oats-review/oats.json +3 -2
  20. package/docs/capabilities.md +229 -58
  21. package/docs/capability-manifest.schema.json +29 -9
  22. package/docs/configuration.md +17 -5
  23. package/docs/conventions.md +18 -28
  24. package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +1 -1
  25. package/docs/design/2026-09-13-knowledge-and-memory-direction.md +3 -3
  26. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +2 -2
  27. package/docs/design/2026-09-15-portable-souls-handoff.md +2 -2
  28. package/docs/design/2026-09-15-portable-souls-implementation.md +1 -1
  29. package/docs/design/2026-09-20-redesign-program-board.md +2 -2
  30. package/docs/design/2026-09-23-workspace-module-contracts.md +1 -1
  31. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +1 -1
  32. package/docs/design/2026-09-24-desktop-phase-f-boundary.md +34 -1
  33. package/docs/design/2026-09-24-phase-d-plan.md +57 -0
  34. package/docs/design/2026-09-25-teams-contract.md +226 -0
  35. package/docs/design/README.md +3 -3
  36. package/docs/design/launch-configurations.md +20 -16
  37. package/docs/design/operations-contract.md +27 -10
  38. package/docs/desktop-cli-api.md +604 -271
  39. package/docs/desktop-instance-start.md +3 -3
  40. package/docs/desktop.md +7 -13
  41. package/docs/execution-targets.md +16 -18
  42. package/docs/first-team.md +15 -18
  43. package/docs/implementation.md +31 -62
  44. package/docs/integrations.md +64 -33
  45. package/docs/knowledge-capability-authoring.md +1 -1
  46. package/docs/knowledge-reference/package-craft.md +10 -8
  47. package/docs/knowledge-theory.md +1 -1
  48. package/docs/knowledge.md +10 -11
  49. package/docs/layers.md +16 -17
  50. package/docs/oats-local.schema.json +30 -1
  51. package/docs/oats-membership.schema.json +5 -3
  52. package/docs/oats-package.schema.json +2 -2
  53. package/docs/oats-workspace.schema.json +1 -1
  54. package/docs/{official-marketplace.md → official-catalog.md} +15 -16
  55. package/docs/packages.md +76 -53
  56. package/docs/release-notes/v0.22.0.md +1 -1
  57. package/docs/release-notes/v0.23.1.md +1 -1
  58. package/docs/release-notes/v0.26.0.md +670 -0
  59. package/docs/release-notes/v0.27.0.md +100 -0
  60. package/docs/schedules.md +54 -132
  61. package/docs/servers.md +4 -4
  62. package/docs/soul.schema.json +11 -4
  63. package/docs/souls-and-instances.md +60 -47
  64. package/docs/workspaces.md +80 -58
  65. package/injects/instance-boundary.md +2 -2
  66. package/injects/work-attached.md +1 -1
  67. package/injects/work-workspace.md +2 -2
  68. package/lib/{portable-files.mjs → bounded-read.mjs} +6 -6
  69. package/lib/{portable-values.mjs → canonical-json.mjs} +3 -12
  70. package/lib/capability-contract.mjs +110 -0
  71. package/lib/config-data.mjs +2 -2
  72. package/lib/core.mjs +947 -5023
  73. package/lib/deprecation.mjs +24 -0
  74. package/lib/digest.mjs +12 -0
  75. package/lib/instance-inspect.mjs +397 -0
  76. package/lib/instance-lifecycle.mjs +3 -4
  77. package/lib/instance-resolution.mjs +212 -26
  78. package/lib/instruction-composition.mjs +0 -20
  79. package/lib/materialize.mjs +6 -4
  80. package/lib/operator-dispatch.mjs +33 -13
  81. package/lib/packages.mjs +25 -190
  82. package/lib/process-group.mjs +1 -1
  83. package/lib/provider-binding.mjs +4 -2
  84. package/lib/provider-reasons.mjs +3 -68
  85. package/lib/remote.mjs +1 -1
  86. package/lib/resolve.mjs +204 -68
  87. package/lib/schedule.mjs +136 -292
  88. package/lib/servers.mjs +70 -38
  89. package/lib/{portable-shape.mjs → shape.mjs} +4 -3
  90. package/lib/tree-copy.mjs +44 -0
  91. package/lib/workspace.mjs +132 -20
  92. package/package-catalog.json +6 -6
  93. package/package.json +1 -1
  94. package/packages/record/lib/session-roots.mjs +8 -6
  95. package/skills/integration-authoring/SKILL.md +48 -40
  96. package/skills/oats-getting-started/SKILL.md +105 -110
  97. package/skills/oats-support/SKILL.md +2 -2
  98. package/skills/soul-craft/SKILL.md +13 -6
  99. package/bin/oats-pi-sdk-host.mjs +0 -17
  100. package/docs/2026-09-03-architecture-proposal.md +0 -642
  101. package/docs/artifact-approvals.schema.json +0 -7
  102. package/docs/captured-invocation-context.schema.json +0 -7
  103. package/docs/captured-resolution.schema.json +0 -7
  104. package/docs/design/package-engine-contract.md +0 -813
  105. package/docs/design/package-runtime-api.md +0 -588
  106. package/docs/desktop-succession.md +0 -57
  107. package/docs/execution-capsule.schema.json +0 -108
  108. package/docs/first-team-demo.md +0 -92
  109. package/docs/knowledge-migration.md +0 -147
  110. package/docs/migration-from-oas.md +0 -103
  111. package/docs/oats-config.schema.json +0 -172
  112. package/docs/oats-lock-v3.schema.json +0 -7
  113. package/docs/oats-lock.schema.json +0 -175
  114. package/docs/operating-team-migration.md +0 -470
  115. package/docs/portable.schema.json +0 -2512
  116. package/docs/provider-check-input.schema.json +0 -7
  117. package/docs/rebuild-to-v2.md +0 -511
  118. package/docs/workspace-adoption.md +0 -74
  119. package/injects/framework-workspace.md +0 -7
  120. package/injects/local-soul.md +0 -19
  121. package/injects/oats-portable.md +0 -20
  122. package/injects/oats.md +0 -11
  123. package/injects/portable-instance-boundary.md +0 -39
  124. package/injects/portable-work-directory.md +0 -29
  125. package/lib/artifact-approvals.mjs +0 -120
  126. package/lib/artifact-tree.mjs +0 -141
  127. package/lib/capability-artifacts.mjs +0 -179
  128. package/lib/capability-execution.mjs +0 -15
  129. package/lib/capability-inputs.mjs +0 -39
  130. package/lib/capability-provenance.mjs +0 -231
  131. package/lib/captured-action-shape.mjs +0 -21
  132. package/lib/captured-admission-shape.mjs +0 -20
  133. package/lib/captured-binding-file.mjs +0 -36
  134. package/lib/captured-dispatch.mjs +0 -66
  135. package/lib/captured-instance-index.mjs +0 -277
  136. package/lib/captured-invocation-context.mjs +0 -130
  137. package/lib/captured-launch-request.mjs +0 -66
  138. package/lib/captured-operation-process.mjs +0 -15
  139. package/lib/captured-pi-custody.mjs +0 -29
  140. package/lib/captured-pi-host.mjs +0 -167
  141. package/lib/captured-pi-outcome.mjs +0 -172
  142. package/lib/captured-resolutions.mjs +0 -275
  143. package/lib/captured-scaffold.mjs +0 -87
  144. package/lib/captured-selector.mjs +0 -28
  145. package/lib/captured-session-backend.mjs +0 -52
  146. package/lib/captured-source-receipt-file.mjs +0 -72
  147. package/lib/helper-injection-policy.mjs +0 -104
  148. package/lib/legacy-lock-codec.mjs +0 -106
  149. package/lib/manifest-settings.mjs +0 -84
  150. package/lib/package-closure.mjs +0 -48
  151. package/lib/package-materialization.mjs +0 -83
  152. package/lib/pi-sdk-host.mjs +0 -229
  153. package/lib/portable-artifacts.mjs +0 -115
  154. package/lib/portable-choices.mjs +0 -82
  155. package/lib/portable-composition.mjs +0 -136
  156. package/lib/portable-digest.mjs +0 -105
  157. package/lib/portable-identity.mjs +0 -40
  158. package/lib/portable-lock.mjs +0 -117
  159. package/lib/portable-onboarding-request.mjs +0 -49
  160. package/lib/portable-onboarding.mjs +0 -256
  161. package/lib/portable-package-preparation.mjs +0 -188
  162. package/lib/portable-policy.mjs +0 -44
  163. package/lib/portable-soul.mjs +0 -42
  164. package/lib/portable-state.mjs +0 -80
  165. package/lib/prepare-composition.mjs +0 -170
  166. package/lib/prepared-bindings.mjs +0 -92
  167. package/lib/prepared-resources.mjs +0 -127
  168. package/lib/provider-binding-broker.mjs +0 -65
  169. package/lib/provider-binding-wire.mjs +0 -116
  170. package/lib/readiness.mjs +0 -225
  171. package/lib/repository-observation.mjs +0 -226
  172. package/lib/resolution-shape.mjs +0 -393
  173. package/lib/schedule-capsule.mjs +0 -206
  174. package/lib/soul-constraints.mjs +0 -40
  175. package/lib/source-projection.mjs +0 -84
  176. package/lib/source-spec.mjs +0 -189
  177. package/lib/workspace-definition.mjs +0 -126
  178. package/lib/workspace-discovery.mjs +0 -146
  179. package/skills/oats/SKILL.md +0 -162
  180. package/skills/oats-config/SKILL.md +0 -164
  181. package/skills/oats-packages/SKILL.md +0 -184
  182. package/skills/oats-portable/SKILL.md +0 -115
  183. package/skills/oats-portable-artifacts/SKILL.md +0 -63
package/lib/schedule.mjs CHANGED
@@ -1,9 +1,10 @@
1
- /** OATS schedules: legacy v1 plus explicit captured-execution v2,
2
- * workspace-scoped definitions and host-owned execution.
1
+ /** OATS schedules: workspace-scoped definitions and host-owned execution.
2
+ * (The captured-execution definitions of 0.24–0.25 were removed in 0.26: a
3
+ * stored one is reported invalid on its job and never runs.)
3
4
  *
4
5
  * A definition lives in <scope>/oats-schedules.json, where the scope is the
5
- * team workspace (the config level declaring the team, else the outermost
6
- * oats-config.yaml level), and is evaluated by the host that holds that
6
+ * deployment directory (the one holding oats-local.yaml, found walking up;
7
+ * see scheduleScopeOf), and is evaluated by the host that holds that
7
8
  * scope: one host registry, one host lock, one host timer that runs
8
9
  * `oats schedule tick --host` once a minute. The tick is a short-lived
9
10
  * process; there is no daemon, no queue and no retry. Only the current
@@ -19,50 +20,49 @@
19
20
  * delivered, started, skipped for wake) and never claim that a task
20
21
  * succeeded. A launch whose side effects cannot be confirmed stays
21
22
  * `unknown` with its slot held until `reconcile` proves what happened. */
22
- import { execFileSync, spawnSync } from "node:child_process";
23
+ import { spawnSync } from "node:child_process";
23
24
  import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
24
25
  import { createHash } from "node:crypto";
25
26
  import { homedir } from "node:os";
26
27
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
27
28
  import { fileURLToPath } from "node:url";
28
29
  import { Cron } from "croner";
29
- import { portableScope } from "./portable-state.mjs";
30
30
  import { loadLocal } from "./workspace.mjs";
31
- import { RESERVED_LAUNCH_ENV, ensureRoot, findAgent, findCapabilityAgent, findInstanceHomes, teamAgentRoots, configChain, spawnInstance, inspectInstanceSession, inputInstanceSession, startInstanceSession, retirePendingMarkerPath, loadCapturedDispatch, prepareCapturedComposition } from "./core.mjs";
32
- import { RECURRENCE_POLICIES, SCHEDULE_ATTEMPT_VERSION, SCHEDULE_DEFINITION_VERSION, admitExecutionTemplate, buildCommandExecutionTemplate, buildWakeExecutionTemplate, capturedDispatchAction, commandArgvWithExecutionBinding, executionContentIntegrity, validateExecutionBinding, validateExecutionCapsule, validateExecutionTemplate, validatePreparationInput } from "./schedule-capsule.mjs";
31
+ import { noteRuntimeName } from "./deprecation.mjs";
32
+ import { RESERVED_LAUNCH_ENV, MAX_INSTANCE_NAME, findAgent, findInstanceHomes, inspectInstanceSession, inputInstanceSession, startInstanceSession, retirePendingMarkerPath } from "./core.mjs";
33
33
 
34
34
  export const SCHEDULE_FILE = "oats-schedules.json";
35
35
  export const SCHEDULE_API = 2;
36
36
  const ID_RE = /^[a-z0-9-]{1,40}$/;
37
- const RUNTIMES = new Set(["pi", "claude", "codex"]);
37
+ const HARNESSES = new Set(["pi", "claude", "codex"]);
38
38
  const BACKENDS = new Set(["tmux", "herdr"]);
39
39
  const MESSAGE_MAX = 256 * 1024;
40
40
  const OATS_BIN = join(dirname(fileURLToPath(import.meta.url)), "..", "bin", "oats.mjs");
41
41
  const COMMAND_TIMEOUT_MS = 5 * 60 * 1000;
42
42
 
43
43
  export function scheduleError(code, message, extra) { return Object.assign(new Error(message), { code, ...(extra || {}) }); }
44
- /** A stored definition that is not even shaped like one (a hand edit): it
45
- * is never observed or executed, and is reported invalid on that job only. */
44
+ /** describe's `executionStatus` (a released wire, kept as it was): a definition is
45
+ * `legacy` — the only kind that runs — or `invalid` (a captured one included,
46
+ * since 0.26), and so is its unresolved attempt. */
46
47
  export function scheduleExecutionStatus(def, intent, lock) {
47
- const versioned = def?.definitionVersion !== undefined || def?.recurrencePolicy !== undefined || def?.execution !== undefined || def?.preparation !== undefined;
48
- let result;
49
- if (!versioned) result = { kind: "legacy", capture: "unknown", migrationRequired: true };
50
- else {
51
- const invalid = shapeError(def);
52
- if (invalid) result = { kind: "invalid", capture: "unknown", migrationRequired: true, reason: invalid };
53
- else if (def.recurrencePolicy === "prepare-on-tick") result = { kind: "prepare-on-tick", capture: "not-captured", migrationRequired: false, reason: "explicit preparation runs at admission" };
54
- else result = { kind: "captured", capture: "recorded", migrationRequired: false, schemaVersion: def.execution.schemaVersion, contentIntegrity: executionContentIntegrity(def.execution), resolution: def.execution.resolution };
55
- }
48
+ const versioned = CAPTURED_SCHEDULE_KEYS.some((key) => def?.[key] !== undefined);
49
+ const result = versioned ? { kind: "invalid", capture: "unknown", migrationRequired: true, reason: shapeError(def) } : { kind: "legacy", capture: "unknown", migrationRequired: true };
56
50
  if (!intent) return result;
57
51
  try {
58
- const execution = validateAttemptExecution(intent, lock);
59
- return { ...result, intent: execution ? { kind: "captured", capture: "recorded", migrationRequired: false, schemaVersion: intent.schemaVersion, executionId: execution.executionId, resolution: execution.resolution }
60
- : { kind: "legacy", capture: "unknown", migrationRequired: true } };
52
+ validateAttemptExecution(intent, lock);
53
+ return { ...result, intent: { kind: "legacy", capture: "unknown", migrationRequired: true } };
61
54
  } catch (error) {
62
55
  return { ...result, intent: { kind: "invalid", capture: "unknown", migrationRequired: true, reason: error.message, errorCode: error.code } };
63
56
  }
64
57
  }
65
58
 
59
+ /** The definition keys of a captured (versioned) schedule. That path was removed in 0.26. */
60
+ export const CAPTURED_SCHEDULE_KEYS = Object.freeze(["definitionVersion", "recurrencePolicy", "execution", "preparation"]);
61
+ const CAPTURED_SCHEDULE_REMOVED = "captured schedules are refused (the captured/portable path was removed in 0.26); remove this job, or re-add it without definitionVersion, recurrencePolicy, execution and preparation";
62
+ /** runtime → harness (0.27.0, lead call 6: read either, write new): a definition's
63
+ * `runtime` is its `harness`. Stored jobs are read with the new name (readDefinitions)
64
+ * and the next write stores it; a job naming both, differently, is invalid on its own. */
65
+ export const HARNESS_CONFLICT = "harness and runtime (its pre-0.27 name) disagree: keep harness: and remove runtime:";
66
66
  export function shapeError(def) {
67
67
  if (!def || typeof def !== "object" || Array.isArray(def)) return "definition is not an object";
68
68
  if (!["spawn", "command", "wake", "operation"].includes(def.kind)) return `kind ${JSON.stringify(def.kind)} is not spawn, command, wake or operation`;
@@ -71,54 +71,30 @@ export function shapeError(def) {
71
71
  if (def.kind === "command" && (typeof def.cwd !== "string" || !Array.isArray(def.argv))) return "cwd and argv are required";
72
72
  if (def.kind === "wake" && (typeof def.home !== "string" || typeof def.message !== "string")) return "home and message are required";
73
73
  if (def.kind === "operation" && (typeof def.operation !== "string" || typeof def.home !== "string")) return "operation and home are required";
74
- if (def.definitionVersion !== undefined || def.recurrencePolicy !== undefined || def.execution !== undefined || def.preparation !== undefined) {
75
- if (def.definitionVersion !== SCHEDULE_DEFINITION_VERSION) return `definitionVersion must be ${SCHEDULE_DEFINITION_VERSION}`;
76
- if (!RECURRENCE_POLICIES.includes(def.recurrencePolicy)) return "recurrencePolicy must be capture or prepare-on-tick";
77
- if (!["command", "wake"].includes(def.kind)) return "versioned execution currently supports command and captured wake definitions only";
78
- if (def.kind === "wake" && def.recurrencePolicy !== "capture") return "wake definitions require capture policy";
79
- if (def.recurrencePolicy === "capture") {
80
- if (def.preparation !== undefined) return "capture policy cannot carry prepare-on-tick input";
81
- try {
82
- validateExecutionTemplate(def.execution);
83
- const expected = def.kind === "command"
84
- ? buildCommandExecutionTemplate({ cwd: def.cwd, argv: def.argv, inputRefs: def.execution.inputRefs, responsibleHuman: def.execution.responsibleHuman })
85
- : buildWakeExecutionTemplate({ home: def.home, message: def.message,
86
- executionBinding: { schemaVersion: 1, deployment: def.execution.deployment, resolution: def.execution.resolution },
87
- inputRefs: def.execution.inputRefs, responsibleHuman: def.execution.responsibleHuman });
88
- if (executionContentIntegrity(expected).value !== executionContentIntegrity(def.execution).value) return "execution template differs from the saved target";
89
- } catch (error) { return `execution template is invalid: ${error.message}`; }
90
- } else {
91
- if (def.execution !== undefined) return "prepare-on-tick cannot carry a pinned execution template";
92
- try { validatePreparationInput(def.preparation); } catch (error) { return `preparation input is invalid: ${error.message}`; }
93
- try { commandArgvWithExecutionBinding(def.argv, { schemaVersion: 1, deployment: def.preparation.deployment, resolution: { schemaVersion: 1, id: `sha256-${"0".repeat(64)}` } }); }
94
- catch (error) { return `prepare-on-tick command is invalid: ${error.message}`; }
95
- }
96
- }
74
+ // A stored captured (versioned) definition is never evaluated or run (removed in 0.26): it reads
75
+ // as invalid, migration required, on that job only.
76
+ if (CAPTURED_SCHEDULE_KEYS.some((key) => def[key] !== undefined)) return CAPTURED_SCHEDULE_REMOVED;
77
+ if (def.runtime !== undefined && def.harness !== undefined && def.runtime !== def.harness) return HARNESS_CONFLICT;
97
78
  return null;
98
79
  }
99
80
 
100
81
  // ---------------------------------------------------------------- scope
101
82
 
102
- /** The one schedule-owning scope for a directory: the config level that
103
- * declares the team (the deployment boundary), else the outermost level
104
- * carrying oats-config.yaml, else the workspace above the agents root. */
83
+ /** The one schedule-owning scope for a directory: the deployment, i.e. the
84
+ * directory holding the oats-local.yaml found walking up. No deployment in
85
+ * reach is E_LOCAL_MISSING; an ambient OATS_ROOT / PI_AGENTS_ROOT never
86
+ * chooses the scope (lead decision c3-5). */
105
87
  export function scheduleScopeOf(dir) {
106
- const start = resolve(dir);
107
- let chain = [];
108
- try { chain = configChain(start); } catch { chain = []; }
109
- const team = chain.find((c) => c.team);
110
- if (team?._level) return resolve(team._level);
111
- if (chain.length) return resolve(chain[chain.length - 1]._level);
112
- try { return dirname(ensureRoot(start)); } catch { return start; }
113
- }
114
- /** Every agents root the scope knows: its own, member repositories', and any
115
- * root that holds instance homes. */
116
- export function scopeRoots(scope) {
117
- const roots = new Set();
118
- try { roots.add(resolve(ensureRoot(scope))); } catch { /* a scope with no agents dir */ }
119
- try { for (const r of teamAgentRoots(scope)) roots.add(resolve(r)); } catch { /* not a team scope */ }
120
- return [...roots];
88
+ let found;
89
+ try { found = loadLocal(dir); }
90
+ catch (e) {
91
+ if (e?.code === "E_LOCAL_MISSING") throw scheduleError("E_LOCAL_MISSING", `schedules belong to a workspace deployment, and none is in reach of ${resolve(dir)} (no oats-local.yaml walking up; \`oats onboard\` creates one)`, { details: { dir: resolve(dir) } });
92
+ throw e;
93
+ }
94
+ return dirname(found.path);
121
95
  }
96
+ /** The scope's one agents root: <deployment>/agents. */
97
+ export function scopeRoots(scope) { return [resolve(scope, "agents")]; }
122
98
 
123
99
  // ------------------------------------------------------------- files
124
100
 
@@ -163,6 +139,14 @@ function writeJson(path, value) {
163
139
  }
164
140
  export function readDefinitions(ws) {
165
141
  const doc = readJsonBounded(definitionsPath(ws), { version: 1, jobs: {} }, "definitions").value;
142
+ if (doc.jobs && typeof doc.jobs === "object" && !Array.isArray(doc.jobs)) {
143
+ for (const [id, def] of Object.entries(doc.jobs)) {
144
+ if (!def || typeof def !== "object" || Array.isArray(def) || def.runtime === undefined) continue;
145
+ noteRuntimeName(`schedule ${id} in ${definitionsPath(ws)} (its next save records harness)`);
146
+ if (def.harness === undefined) { const { runtime, ...rest } = def; doc.jobs[id] = { ...rest, harness: runtime }; }
147
+ else if (def.harness === def.runtime) { const { runtime, ...rest } = def; void runtime; doc.jobs[id] = rest; }
148
+ }
149
+ }
166
150
  if (![1, 2].includes(doc.version) || typeof doc.jobs !== "object" || doc.jobs === null || Array.isArray(doc.jobs)) throw scheduleError("E_SCHEDULE_INVALID", `${definitionsPath(ws)} must be {version: 1|2, jobs: {}}`, { field: "file" });
167
151
  return doc;
168
152
  }
@@ -321,9 +305,18 @@ export function validateDefinition(ws, def, { checkAgent = true } = {}) {
321
305
  if (def.repo !== undefined) { if (typeof def.repo !== "string" || !def.repo.trim()) throw scheduleError("E_SCHEDULE_INVALID", "repo: the work repository path, as oats spawn --repo", { field: "repo" }); out.repo = def.repo; }
322
306
  if (def.backend !== undefined) { if (!BACKENDS.has(def.backend)) throw scheduleError("E_SCHEDULE_INVALID", "backend: tmux or herdr", { field: "backend" }); out.backend = def.backend; }
323
307
  if (def.purpose !== undefined) { if (typeof def.purpose !== "string" || !/^[a-z0-9-]{1,40}$/.test(def.purpose)) throw scheduleError("E_SCHEDULE_INVALID", "purpose: lowercase letters, digits and dashes", { field: "purpose" }); out.purpose = def.purpose; }
308
+ // A run spawns <agent>-<purpose|id>-<YYYYMMDDHHMM>; instance names are at most
309
+ // MAX_INSTANCE_NAME characters, so refuse here what would fail at every firing.
310
+ { const stem = out.purpose ?? id, derived = `${out.agent}-${stem}-${"0".repeat(12)}`;
311
+ if (derived.length > MAX_INSTANCE_NAME) throw scheduleError("E_SCHEDULE_INVALID", `${out.purpose !== undefined ? "purpose" : "id"}: runs would be named ${out.agent}-${stem}-<YYYYMMDDHHMM> (${derived.length} characters); instance names are at most ${MAX_INSTANCE_NAME} characters — shorten the ${out.purpose !== undefined ? "purpose" : "schedule id (or set a shorter purpose)"}`, { field: out.purpose !== undefined ? "purpose" : "id" }); }
324
312
  if (typeof def.task !== "string" || !def.task.trim()) throw scheduleError("E_SCHEDULE_INVALID", "task: non-empty text", { field: "task" });
325
313
  out.task = def.task;
326
- if (def.runtime !== undefined) { if (!RUNTIMES.has(def.runtime)) throw scheduleError("E_SCHEDULE_INVALID", "runtime: pi, claude or codex", { field: "runtime" }); out.runtime = def.runtime; }
314
+ if (def.runtime !== undefined) {
315
+ if (def.harness !== undefined && def.harness !== def.runtime) throw scheduleError("E_SCHEDULE_INVALID", HARNESS_CONFLICT, { field: "runtime" });
316
+ noteRuntimeName(`schedule ${id}: runtime (written as harness)`);
317
+ def = { ...def, harness: def.runtime };
318
+ }
319
+ if (def.harness !== undefined) { if (!HARNESSES.has(def.harness)) throw scheduleError("E_SCHEDULE_INVALID", "harness: pi, claude or codex", { field: "harness" }); out.harness = def.harness; }
327
320
  if (def.model !== undefined) { if (typeof def.model !== "string") throw scheduleError("E_SCHEDULE_INVALID", "model: string", { field: "model" }); out.model = def.model; }
328
321
  if (def.yolo !== undefined) { if (typeof def.yolo !== "boolean") throw scheduleError("E_SCHEDULE_INVALID", "yolo: boolean", { field: "yolo" }); out.yolo = def.yolo; }
329
322
  if (def.wake !== undefined) {
@@ -337,6 +330,8 @@ export function validateDefinition(ws, def, { checkAgent = true } = {}) {
337
330
  if (typeof def.cwd !== "string" || !isAbsolute(def.cwd) || !inside(ws, def.cwd) || !existsSync(def.cwd)) throw scheduleError("E_SCHEDULE_INVALID", "cwd: an existing absolute directory inside the scope", { field: "cwd" });
338
331
  if (!Array.isArray(def.argv) || !def.argv.length || def.argv.some((a) => typeof a !== "string" || a.includes("\0")) || def.argv[0] !== "oats") throw scheduleError("E_SCHEDULE_INVALID", "argv: oats-only argument list starting with \"oats\"", { field: "argv" });
339
332
  if (def.argv[1] === "schedule") throw scheduleError("E_SCHEDULE_INVALID", "argv: a command job may not run oats schedule (it would wait on the host lock the tick holds)", { field: "argv" });
333
+ const selector = def.argv.slice(1, def.argv.includes("--") ? def.argv.indexOf("--") : undefined).find((a) => /^--(deployment|resolution|artifact-set)(=|$)/.test(a));
334
+ if (selector) throw scheduleError("E_SCHEDULE_INVALID", `argv: ${selector.split("=")[0]} is a captured selector (the captured/portable path was removed in 0.26) — the job would be refused at every firing`, { field: "argv" });
340
335
  out.cwd = resolve(def.cwd); out.argv = [...def.argv];
341
336
  } else if (def.kind === "wake") {
342
337
  if (typeof def.home !== "string" || !isAbsolute(def.home) || !inside(ws, def.home) || !existsSync(join(def.home, "instance.json"))) throw scheduleError("E_SCHEDULE_INVALID", "home: an existing instance home inside the scope", { field: "home" });
@@ -350,61 +345,19 @@ export function validateDefinition(ws, def, { checkAgent = true } = {}) {
350
345
  if (typeof def.home !== "string" || !isAbsolute(def.home) || !inside(ws, def.home) || !existsSync(join(def.home, "instance.json"))) throw scheduleError("E_SCHEDULE_INVALID", "home: an existing instance home inside the scope", { field: "home" });
351
346
  out.operation = def.operation; out.home = resolve(def.home);
352
347
  } else throw scheduleError("E_SCHEDULE_INVALID", "kind: spawn, command, wake or operation", { field: "kind" });
353
- const versioned = def.definitionVersion !== undefined || def.recurrencePolicy !== undefined || def.execution !== undefined || def.preparation !== undefined;
354
- if (versioned) {
355
- if (def.definitionVersion !== SCHEDULE_DEFINITION_VERSION) throw scheduleError("E_SCHEDULE_INVALID", `definitionVersion: ${SCHEDULE_DEFINITION_VERSION} required for captured schedules`, { field: "definitionVersion" });
356
- if (!RECURRENCE_POLICIES.includes(def.recurrencePolicy)) throw scheduleError("E_SCHEDULE_INVALID", "recurrencePolicy: capture or prepare-on-tick", { field: "recurrencePolicy" });
357
- if (!["command", "wake"].includes(def.kind)) throw scheduleError("E_SCHEDULE_INVALID", "versioned execution currently supports command and captured wake jobs; spawn and operation await their captured consumer adapters", { field: "recurrencePolicy" });
358
- if (def.kind === "wake" && def.recurrencePolicy !== "capture") throw scheduleError("E_SCHEDULE_INVALID", "versioned wake jobs require capture policy", { field: "recurrencePolicy" });
359
- out.definitionVersion = SCHEDULE_DEFINITION_VERSION;
360
- out.recurrencePolicy = def.recurrencePolicy;
361
- if (def.recurrencePolicy === "capture") {
362
- if (def.preparation !== undefined) throw scheduleError("E_SCHEDULE_INVALID", "preparation belongs only to prepare-on-tick", { field: "preparation" });
363
- let execution;
364
- try {
365
- const responsibleHuman = Object.hasOwn(def, "responsibleHuman") ? def.responsibleHuman : def.execution?.responsibleHuman;
366
- if (responsibleHuman === undefined) throw new Error("responsibleHuman must be explicit for captured execution (null means messaging disabled)");
367
- if (def.kind === "command") execution = buildCommandExecutionTemplate({ cwd: out.cwd, argv: out.argv,
368
- inputRefs: def.inputRefs ?? def.execution?.inputRefs ?? {}, responsibleHuman });
369
- else {
370
- const meta = readJson(join(out.home, "instance.json"), null);
371
- if (!meta?.executionBinding) throw scheduleError("migration-required", "wake target has no captured executionBinding", { field: "executionBinding" });
372
- validateExecutionBinding(meta.executionBinding);
373
- execution = buildWakeExecutionTemplate({ home: out.home, message: out.message, executionBinding: meta.executionBinding,
374
- inputRefs: def.inputRefs ?? def.execution?.inputRefs ?? {}, responsibleHuman });
375
- }
376
- if (def.execution !== undefined) {
377
- validateExecutionTemplate(def.execution);
378
- if (executionContentIntegrity(def.execution).value !== executionContentIntegrity(execution).value) throw new Error("template differs from target, binding, inputs or responsible human");
379
- }
380
- } catch (error) {
381
- if (error.code === "migration-required") throw error;
382
- throw scheduleError("E_SCHEDULE_INVALID", `execution: ${error.message}`, { field: "execution" });
383
- }
384
- out.execution = execution;
385
- } else {
386
- if (def.execution !== undefined) throw scheduleError("E_SCHEDULE_INVALID", "execution: prepare-on-tick captures only when a future tick is admitted", { field: "execution" });
387
- try {
388
- out.preparation = validatePreparationInput(def.preparation);
389
- commandArgvWithExecutionBinding(out.argv, { schemaVersion: 1, deployment: out.preparation.deployment, resolution: { schemaVersion: 1, id: `sha256-${"0".repeat(64)}` } });
390
- } catch (error) { throw scheduleError("E_SCHEDULE_INVALID", `preparation: ${error.message}`, { field: "preparation" }); }
391
- }
392
- }
348
+ // Captured (versioned) schedules were removed in 0.26 (lead decisions on (e), D4).
349
+ const captured = CAPTURED_SCHEDULE_KEYS.filter((key) => def[key] !== undefined);
350
+ if (captured.length) throw scheduleError("E_SCHEDULE_INVALID", `${captured.join(", ")}: ${CAPTURED_SCHEDULE_REMOVED}`, { field: captured[0] });
393
351
  return out;
394
352
  }
395
353
 
396
- /** The soul a spawn job launches: the exact agents root the definition
397
- * names (agentsRoot, what tells same-named souls in different member
398
- * repositories apart), else the scope's own root; the same lookups `oats
399
- * spawn` uses (local and persistent souls, then capability-declared
400
- * agents). `repo` is the work repository and never selects a soul. */
354
+ /** The soul a spawn job launches, under the deployment's one agents root
355
+ * (a definition's agentsRoot, when given, must be that root). `repo` is the
356
+ * work repository and never selects a soul. */
401
357
  export function resolveScheduledAgent(ws, def) {
402
- let root;
403
- try { root = def.agentsRoot ? ensureRoot(def.agentsRoot) : ensureRoot(ws); }
404
- catch (e) { throw scheduleError("E_SCHEDULE_INVALID", `agentsRoot: ${e.message}`, { field: def.agentsRoot ? "agentsRoot" : "agent" }); }
405
- if (def.agentsRoot && resolve(root) !== resolve(def.agentsRoot)) throw scheduleError("E_SCHEDULE_INVALID", `agentsRoot: ${def.agentsRoot} is not an agents root (resolved to ${root})`, { field: "agentsRoot" });
406
- let agent = findAgent(root, def.agent);
407
- if (!agent) { try { agent = findCapabilityAgent(dirname(root), root, def.agent); } catch { agent = undefined; } }
358
+ const [root] = scopeRoots(ws);
359
+ if (def.agentsRoot && resolve(def.agentsRoot) !== root) throw scheduleError("E_SCHEDULE_INVALID", `agentsRoot: ${def.agentsRoot} is not this deployment's agents root (${root})`, { field: "agentsRoot" });
360
+ const agent = findAgent(root, def.agent);
408
361
  if (!agent) throw scheduleError("E_SCHEDULE_INVALID", `agent: no soul named ${JSON.stringify(def.agent)} under ${root}`, { field: "agent" });
409
362
  return { root, agent };
410
363
  }
@@ -418,13 +371,13 @@ export function nextRunAfter(def, from) { const n = cronOf(def).nextRun(from); r
418
371
  /** Due exactly at this minute: the first run after (minute - 1 s) is this minute. */
419
372
  export function dueAt(def, minute) { const n = cronOf(def).nextRun(new Date(minute.getTime() - 1000)); return !!n && n.getTime() === minute.getTime(); }
420
373
  export function purposeSuffix(minute) { return minute.toISOString().slice(0, 16).replace(/[-T:]/g, ""); }
421
- /** Ordering key for admission: when this job last launched a runtime. */
374
+ /** Ordering key for admission: when this job last launched a harness. */
422
375
  export function launchOrderKey(js) { return String(js?.lastLaunchedAt || ""); }
423
376
 
424
377
  // ---------------------------------------------------------- observation
425
378
 
426
379
  /** What a run's instance looks like now. Home gone = ended; a home that is
427
- * retiring is still there (its runtime may be alive pending teardown) and
380
+ * retiring is still there (its harness may be alive pending teardown) and
428
381
  * keeps its slot; present but no running harness = stopped (needs
429
382
  * attention, never removed); running = active; not inspectable = unknown. */
430
383
  export function observeHome(home, io) {
@@ -437,7 +390,7 @@ export function observeHome(home, io) {
437
390
  if (retiring) return { outcome: "active", note: "retiring: home present, teardown pending" };
438
391
  // A shell right after `session start` is that launch's startup phase
439
392
  // until its wrapper writes the exit marker for the launch id; only then
440
- // (or with no start receipt at all) is a shell a finished runtime.
393
+ // (or with no start receipt at all) is a shell a finished harness.
441
394
  if (s.present && s.state === "shell" && startupInProgress(home)) return { outcome: "starting" };
442
395
  return { outcome: "stopped" };
443
396
  } catch (e) { return { outcome: "unknown", error: e.message }; }
@@ -452,7 +405,7 @@ function startupInProgress(home) {
452
405
  }
453
406
 
454
407
  /** All homes for an instance name across the scope's roster (every known
455
- * agents root and its local-agents sibling). */
408
+ * agents root). */
456
409
  export function findHomesInScope(ws, instance) {
457
410
  const homes = new Set();
458
411
  for (const root of scopeRoots(ws)) {
@@ -465,8 +418,6 @@ export function findHomesInScope(ws, instance) {
465
418
  const h = join(root, e.name, "instances", instance);
466
419
  if (existsSync(join(h, "instance.json"))) homes.add(resolve(h));
467
420
  }
468
- const local = join(dirname(root), "local-agents");
469
- if (existsSync(local)) for (const e of readdirSync(local, { withFileTypes: true })) { if (!e.isDirectory()) continue; const h = join(local, e.name, "instances", instance); if (existsSync(join(h, "instance.json"))) homes.add(resolve(h)); }
470
421
  }
471
422
  return [...homes];
472
423
  }
@@ -485,27 +436,17 @@ function childEnv() {
485
436
  for (const key of [...RESERVED_LAUNCH_ENV,
486
437
  "OATS_DEPLOYMENT", "OATS_RESOLUTION", "OATS_CAPABILITY", "OATS_LAYER", "OATS_LEVEL", "OATS_META", "OATS_OPERATION",
487
438
  "OATS_REPO", "OATS_BRANCH", "OATS_WORK", "OATS_KIND", "OATS_TASK",
488
- "OATS_RUNTIME", "OATS_PREVIOUS_RUNTIME", "OATS_RETIRE_INTENT",
439
+ "OATS_HARNESS", "OATS_PREVIOUS_HARNESS", "OATS_RUNTIME", "OATS_PREVIOUS_RUNTIME", "OATS_RETIRE_INTENT",
489
440
  "OATS_TEAM_NAME", "OATS_TEAM_ID", "OATS_TEAM_SCOPE",
490
441
  ]) delete env[key];
491
442
  return env;
492
443
  }
493
- /** Is this spawn a WORKSPACE-model spawn (module contract §5, decision 7)?
494
- * Two marks, either suffices: the deployment realizes a workspace
495
- * (`oats-local.yaml` walking up from the agents root or the scope — the same
496
- * detection `oats spawn` uses), or the soul under the agents root was copied
497
- * from a member repo by ensureWorkspaceSoul (`.oats-soul-source.json` beside
498
- * it). An unreadable oats-local.yaml is still a workspace deployment: the
499
- * spawn must go the workspace way and report E_WORKSPACE_SCHEMA, never fall
500
- * back to a classic home. Returns null for a classic (bare agents root) spawn. */
501
- export function workspaceSpawnContext(ws, root, agent) {
502
- for (const dir of [dirname(root), ws]) {
503
- try { const found = loadLocal(dir); return { kind: "local", local: found.path, dir: dirname(found.path) }; }
504
- catch (e) { if (e?.code !== "E_LOCAL_MISSING") return { kind: "local", local: null, dir, error: { code: e.code || "E_WORKSPACE_SCHEMA", message: e.message } }; }
505
- }
506
- const stamp = agent?._dir ? join(agent._dir, ".oats-soul-source.json") : join(root, agent?.name || "", ".oats-soul-source.json");
507
- if (agent?.name && existsSync(stamp)) return { kind: "soul-source", local: null, dir: dirname(root), stamp };
508
- return null;
444
+ /** The deployment a spawn job runs in (module contract §5, decision 7): the
445
+ * scope's oats-local.yaml. An unreadable one is reported (E_WORKSPACE_SCHEMA)
446
+ * by the spawn, never answered some other way. */
447
+ export function workspaceSpawnContext(ws) {
448
+ try { const found = loadLocal(ws); return { kind: "local", local: found.path, dir: dirname(found.path) }; }
449
+ catch (e) { return { kind: "local", local: null, dir: ws, error: { code: e.code || "E_WORKSPACE_SCHEMA", message: e.message } }; }
509
450
  }
510
451
  /** A workspace spawn materializes EXACTLY like `oats spawn` — prepareInstance →
511
452
  * ensureWorkspaceSoul → spawnInstanceAsync (resolve over the remotes, copy every
@@ -521,7 +462,7 @@ export function workspaceSpawnContext(ws, root, agent) {
521
462
  function spawnViaCli(ws, root, agent, opts, context, io) {
522
463
  if (context.error) throw scheduleError(context.error.code, `this deployment realizes a workspace (${context.dir}) but its oats-local.yaml cannot be read: ${context.error.message}`);
523
464
  const argv = ["spawn", agent.name, "--dir", context.dir, "--agents-root", root, "--purpose", opts.purpose, "--json"];
524
- if (opts.runtime) argv.push("--runtime", opts.runtime);
465
+ if (opts.harness) argv.push("--harness", opts.harness);
525
466
  if (opts.model) argv.push("--model", opts.model);
526
467
  if (opts.backend) argv.push("--backend", opts.backend);
527
468
  if (opts.repo) argv.push("--repo", opts.repo);
@@ -551,20 +492,16 @@ function spawnViaCli(ws, root, agent, opts, context, io) {
551
492
  return { instance: res.instance, home: res.home, launched: res.launched, materialized: "workspace" };
552
493
  }
553
494
  /** Launch one spawn job. `io.spawn(root, agent, opts)` overrides the whole
554
- * launcher for tests. Otherwise a workspace deployment goes through the
555
- * kernel's own `oats spawn` (workspace materialization: `.oats/modules`,
556
- * module skills and injects — never a classic home); a bare agents root goes
557
- * through the synchronous classic spawnInstance, whose failures throw here. */
495
+ * launcher for tests. Otherwise the kernel's own `oats spawn` runs in the
496
+ * deployment (workspace materialization: `.oats/modules`, module skills and
497
+ * injects). */
558
498
  function launchSpawn(ws, def, minute, io) {
559
499
  const { root, agent } = resolveScheduledAgent(ws, def);
560
500
  const purpose = `${def.purpose || def.id}-${purposeSuffix(minute)}`;
561
- const opts = { task: def.task.trimEnd() + scheduleBlock(def, minute), purpose, runtime: def.runtime, model: def.model, yolo: def.yolo, backend: def.backend, repo: def.repo, launch: io?.noLaunch === true ? false : true };
501
+ const opts = { task: def.task.trimEnd() + scheduleBlock(def, minute), purpose, harness: def.harness, model: def.model, yolo: def.yolo, backend: def.backend, repo: def.repo, launch: io?.noLaunch === true ? false : true };
562
502
  let r;
563
503
  if (io?.spawn) r = io.spawn(root, agent, opts);
564
- else {
565
- const context = workspaceSpawnContext(ws, root, agent);
566
- r = context ? spawnViaCli(ws, root, agent, opts, context, io) : spawnInstance(root, agent, opts);
567
- }
504
+ else r = spawnViaCli(ws, root, agent, opts, workspaceSpawnContext(ws), io);
568
505
  const run = { instance: r.instance, home: r.home, launched: !!r.instance, kind: "spawn", ...(r.materialized ? { materialized: r.materialized } : {}) };
569
506
  if (def.wake && r.home) {
570
507
  try { run.wakeSchedule = saveWakeForHome(ws, { instance: r.instance, home: r.home, wake: def.wake }); }
@@ -629,74 +566,21 @@ function launchCommand(ws, def, io) {
629
566
  return { kind: "command", launched: envelope.ok === true, ...(instance ? { instance } : {}), ...(home ? { home, incarnation: incarnationOfHome(home) } : {}), ...(server ? { server } : {}), ...(envelope.ok ? {} : { error: envelope.error?.message || "command failed", errorCode: envelope.error?.code }) };
630
567
  }
631
568
 
632
- /** Verify an immutable capsule and its current exact-artifact authorization
633
- * before the scheduler acquires a slot or invokes a child. Legacy definitions
634
- * retain their old release boundary; prepare-on-tick remains fail-closed until
635
- * the preparation owner supplies its transaction API. */
636
- function homeExecutionBinding(home) {
637
- const meta = readJson(join(home, "instance.json"), null);
638
- if (!meta?.executionBinding) throw scheduleError("migration-required", "wake target has no captured executionBinding");
639
- try { return validateExecutionBinding(meta.executionBinding); }
640
- catch (error) { throw scheduleError("E_SCHEDULE_INVALID", `wake target executionBinding is invalid: ${error.message}`); }
641
- }
569
+ /** A captured attempt or job lock left by 0.24–0.25 (the captured path, removed in
570
+ * 0.26) is never run, adopted or released here: only removing its job clears it. */
571
+ const CAPTURED_ATTEMPT_REMOVED = "this job holds a captured schedule attempt (the captured/portable path was removed in 0.26); remove the job (oats schedule remove --force <id>)";
642
572
  function validateAttemptExecution(attempt, lock) {
643
- if (attempt?.execution === undefined) {
644
- if (attempt?.schemaVersion !== undefined) throw scheduleError("migration-required", "versioned scheduled attempt has no captured execution");
645
- if (lock?.executionId !== undefined) throw scheduleError("E_SCHEDULE_INVALID", "captured job lock has no matching admitted capsule");
646
- return undefined;
647
- }
648
- if (attempt.schemaVersion !== SCHEDULE_ATTEMPT_VERSION) throw scheduleError("migration-required", "scheduled attempt uses an unsupported version");
649
- const execution = validateExecutionCapsule(attempt.execution);
650
- if (lock && lock.executionId !== execution.executionId) throw scheduleError("E_SCHEDULE_INVALID", "job lock does not name the admitted execution intent");
651
- return execution;
573
+ if (attempt?.execution !== undefined || attempt?.schemaVersion !== undefined) throw scheduleError("migration-required", CAPTURED_ATTEMPT_REMOVED);
574
+ if (lock?.executionId !== undefined) throw scheduleError("E_SCHEDULE_INVALID", `captured job lock: ${CAPTURED_ATTEMPT_REMOVED}`);
652
575
  }
653
576
  function validateActiveExecution(js, lock) {
654
577
  if (lock?.unreadable) throw scheduleError("E_SCHEDULE_INVALID", "job lock custody is unreadable");
655
578
  if (js.attempt) return validateAttemptExecution(js.attempt, lock);
656
- if (lock && (js.lastRun?.execution || lock.executionId !== undefined)) {
657
- return validateAttemptExecution(js.lastRun?.execution ? { schemaVersion: SCHEDULE_ATTEMPT_VERSION, execution: js.lastRun.execution } : {}, lock);
658
- }
659
- }
660
- function receiptMatchesExecution(receipt, execution) {
661
- return !!receipt?.execution && receipt.execution.executionId === execution.executionId
662
- && executionContentIntegrity(validateExecutionCapsule(receipt.execution)).value === executionContentIntegrity(execution).value;
663
- }
664
- function admitExecution(def, io, { createIntent = true } = {}) {
665
- if (def.definitionVersion === undefined) return undefined;
666
- if (def.definitionVersion !== SCHEDULE_DEFINITION_VERSION) throw scheduleError("migration-required", "scheduled definition uses an unsupported execution version");
667
- let execution;
668
- if (def.recurrencePolicy === "prepare-on-tick") {
669
- if (!createIntent) return undefined; // dry-run never prepares or publishes
670
- const prepare = io && Object.hasOwn(io, "prepare") ? io.prepare : prepareCapturedComposition;
671
- if (typeof prepare !== "function") throw scheduleError("migration-required", "prepare-on-tick needs the preparation transaction adapter before this tick can be admitted");
672
- const request = validatePreparationInput(def.preparation);
673
- const requestedDeployment = portableScope(request.deployment); // Freeze identity before preparation, including platform path aliases.
674
- const prepared = prepare(request);
675
- if (!prepared || typeof prepared !== "object" || !prepared.executionBinding || !Object.hasOwn(prepared, "responsibleHuman")) {
676
- throw scheduleError("needs-configuration", "preparation did not produce a complete executionBinding and responsibleHuman", { problems: prepared?.problems || [] });
677
- }
678
- const binding = validateExecutionBinding(prepared.executionBinding);
679
- let sameDeployment = false;
680
- try { sameDeployment = portableScope(binding.deployment) === requestedDeployment && portableScope(request.deployment) === requestedDeployment; } catch { /* absent/changed scope is not the admitted deployment */ }
681
- if (!sameDeployment) throw scheduleError("E_SCHEDULE_INVALID", "prepared executionBinding changed the requested deployment");
682
- binding.deployment = requestedDeployment; // Persist canonical authority, not a retargetable spelling.
683
- if (prepared.resolution && prepared.resolution.id !== binding.resolution.id) throw scheduleError("E_SCHEDULE_INVALID", "prepared resolution differs from executionBinding");
684
- const argv = commandArgvWithExecutionBinding(def.argv, binding);
685
- execution = buildCommandExecutionTemplate({ cwd: def.cwd, argv, inputRefs: prepared.inputRefs ?? {}, responsibleHuman: prepared.responsibleHuman });
686
- } else execution = validateExecutionTemplate(def.execution);
687
- const action = capturedDispatchAction(execution);
688
- if (action.kind === "wake") {
689
- const binding = homeExecutionBinding(execution.target.home);
690
- if (binding.deployment !== execution.deployment || binding.resolution.id !== execution.resolution.id) throw scheduleError("E_SCHEDULE_INVALID", "wake target executionBinding differs from its captured template");
691
- }
692
- const request = { deployment: execution.deployment, resolution: execution.resolution, action };
693
- (io?.admit || loadCapturedDispatch)(request);
694
- if (action.kind === "wake") throw scheduleError("migration-required", "captured wake lifecycle dispatch is not integrated yet");
695
- return createIntent ? admitExecutionTemplate(execution) : undefined;
579
+ if (lock && (js.lastRun?.execution !== undefined || lock.executionId !== undefined)) validateAttemptExecution({ execution: js.lastRun?.execution }, lock);
696
580
  }
697
581
  /** One wake. `startIfStopped` false between due minutes (a harness that
698
582
  * keeps exiting is not restarted every minute); `canStart` false when the
699
- * host bound is reached (a new runtime takes a slot; a delivery does not). */
583
+ * host bound is reached (a new harness takes a slot; a delivery does not). */
700
584
  /** A failure message that reports effects the kernel could not undo or
701
585
  * confirm (spawn compensation's "rollback INCOMPLETE", a quarantined or
702
586
  * retained home): the launch's effects are unconfirmed, never a confirmed
@@ -715,14 +599,14 @@ function performWake(def, io, { startIfStopped = true, canStart = true, reserve
715
599
  // The slot is persisted BEFORE the start call and kept on ANY start
716
600
  // exception: an error code does not prove nothing was allocated (core
717
601
  // can refuse while recording, after the session exists). The next
718
- // observation releases it once the runtime is proven stopped or absent;
602
+ // observation releases it once the harness is proven stopped or absent;
719
603
  // at worst a refused start occupies a slot for one tick.
720
604
  if (reserve && !reserve()) return { kind: "wake", action: "skipped", reason: "the job's slot is held by another process; delivery pending", pending: true };
721
605
  try {
722
606
  const r = (io?.start || startInstanceSession)(def.home);
723
- return { kind: "wake", action: "started", instance: r.instance, target: r.target, home: def.home, incarnation: incarnationOfHome(def.home), reason: "session was stopped; the message is pending until the session is active", pending: true, startedRuntime: true };
607
+ return { kind: "wake", action: "started", instance: r.instance, target: r.target, home: def.home, incarnation: incarnationOfHome(def.home), reason: "session was stopped; the message is pending until the session is active", pending: true, startedHarness: true };
724
608
  } catch (e) {
725
- return { kind: "wake", action: "skipped", reason: `start did not complete (${e.code || "error"}): ${e.message}; the slot is kept until the session is observed stopped or absent`, error: e.message, errorCode: e.code, pending: true, startedRuntime: "unconfirmed" };
609
+ return { kind: "wake", action: "skipped", reason: `start did not complete (${e.code || "error"}): ${e.message}; the slot is kept until the session is observed stopped or absent`, error: e.message, errorCode: e.code, pending: true, startedHarness: "unconfirmed" };
726
610
  }
727
611
  }
728
612
  try {
@@ -737,15 +621,18 @@ function performWake(def, io, { startIfStopped = true, canStart = true, reserve
737
621
 
738
622
  /** Observe the previous run of one job. The tracked identity is what was
739
623
  * launched (the lock's or lastRun's home), never the current definition,
740
- * so an edit cannot make a slot forget its runtime. A wake slot is held
741
- * while its started runtime is active, starting, unknown or retiring and
742
- * released when that runtime is proven stopped (the start receipt's exit
624
+ * so an edit cannot make a slot forget its harness. A wake slot is held
625
+ * while its started harness is active, starting, unknown or retiring and
626
+ * released when that harness is proven stopped (the start receipt's exit
743
627
  * marker) or its home is gone: a persistent home outliving its process
744
628
  * does not keep a slot. */
745
629
  function observePrevious(ws, id, def, js, io, now, { mutate }) {
630
+ // An invalid definition (a stored captured one included) is reported on its
631
+ // job by the tick and never observed.
632
+ if (shapeError(def)) return;
746
633
  const lock = jobLockInfo(ws, id);
747
634
  validateActiveExecution(js, lock); // No observation may erase invalid custody.
748
- if (js.attempt || shapeError(def)) return; // Unconfirmed effects need reconcile.
635
+ if (js.attempt) return; // Unconfirmed effects need reconcile.
749
636
  const lr = js.lastRun;
750
637
  const home = def.kind === "wake" ? (lock?.home || lr?.home || def.home) : (lock?.home || lr?.home);
751
638
  const tracked = def.kind === "wake" ? !!lock : !!home && ["launched", "active", "starting", "stopped", "unknown"].includes(lr?.outcome);
@@ -756,7 +643,7 @@ function observePrevious(ws, id, def, js, io, now, { mutate }) {
756
643
  if (seen.outcome === "ended" || seen.outcome === "stopped") {
757
644
  releaseJobLock(ws, id);
758
645
  if (seen.outcome === "ended") delete js.pendingWake;
759
- if (lr) js.lastRun = { ...lr, ...(seen.outcome === "ended" ? { endedAt: now.toISOString() } : { stoppedAt: now.toISOString() }), note: seen.outcome === "ended" ? "started runtime ended" : "started runtime stopped; slot released" };
646
+ if (lr) js.lastRun = { ...lr, ...(seen.outcome === "ended" ? { endedAt: now.toISOString() } : { stoppedAt: now.toISOString() }), note: seen.outcome === "ended" ? "started harness ended" : "started harness stopped; slot released" };
760
647
  }
761
648
  return;
762
649
  }
@@ -786,41 +673,24 @@ export function tickWorkspace(ws, { now = new Date(), io, reg, wsList, dryRun =
786
673
  // `candidates` (the host tick) means the host already observed every scope.
787
674
  if (!candidates) for (const [id, def] of Object.entries(defs.jobs)) observePrevious(ws, id, def, stateOf(id), io, now, { mutate: !dryRun });
788
675
  if (observeOnly) { if (!dryRun) writeState(ws, st); return considered; }
789
- // Least recently LAUNCHED first: only an actual runtime launch counts, a
676
+ // Least recently LAUNCHED first: only an actual harness launch counts, a
790
677
  // skipped or pending job keeps its place at the front of the line.
791
678
  const order = (candidates || Object.keys(defs.jobs)).filter((id) => Object.prototype.hasOwnProperty.call(defs.jobs, id) && (!only || id === only)).sort((a, b) => launchOrderKey(st.jobs[a]).localeCompare(launchOrderKey(st.jobs[b])) || a.localeCompare(b));
792
679
  for (const id of order) {
793
680
  const def = defs.jobs[id];
794
681
  const js = stateOf(id);
795
- validateActiveExecution(js, jobLockInfo(ws, id)); // Also covers host pre-observed candidates.
796
682
  let due, cronError;
797
683
  const shape = shapeError(def);
684
+ if (!shape) validateActiveExecution(js, jobLockInfo(ws, id)); // Also covers host pre-observed candidates.
798
685
  if (shape) { cronError = shape; due = false; }
799
686
  else { try { due = only ? true : dueAt(def, minute); } catch (e) { cronError = e.message; due = false; } }
800
687
  if (cronError) { if (!dryRun) js.lastRun = { ...(js.lastRun || {}), outcome: "invalid", error: `definition cannot be evaluated: ${cronError}` }; record(id, "invalid", { error: cronError }); continue; }
801
- // A captured wake is bound to the target home's exact executionBinding,
802
- // but remains gated until lifecycle exposes captured wake dispatch. Keep
803
- // this before both pending delivery and due-start branches so neither can
804
- // fall through to the legacy home path.
805
- const versionedWakeDue = def.kind === "wake" && def.definitionVersion !== undefined && !dryRun
806
- && (def.enabled || only) && ((js.pendingWake && !only) || only || (due && js.lastAttemptedMinute !== key));
807
- if (versionedWakeDue) {
808
- let error;
809
- try { admitExecution(def, io, { createIntent: false }); } catch (caught) { error = caught; }
810
- if (error) {
811
- if (only || due) js.lastAttemptedMinute = key;
812
- js.lastRun = { scheduledFor: minute.toISOString(), startedAt: now.toISOString(), kind: "wake", launched: false, outcome: "blocked", error: error.message, errorCode: error.code || "E_SCHEDULE_ADMISSION" };
813
- record(id, "blocked", { error: error.message, errorCode: error.code || "E_SCHEDULE_ADMISSION" });
814
- continue;
815
- }
816
- throw scheduleError("migration-required", "captured wake unexpectedly passed its lifecycle integration gate");
817
- }
818
688
  if (def.kind === "wake" && js.pendingWake && def.enabled && !dryRun && !only) {
819
689
  const dueNow = due && js.lastAttemptedMinute !== key;
820
690
  const held = !!jobLockInfo(ws, id);
821
691
  const canStart = held || liveLocks(wsAll) < bound;
822
692
  const run = performWake(def, io, { startIfStopped: dueNow, canStart, reserve: () => held || acquireJobLock(ws, id, { scheduledFor: js.pendingWake.scheduledFor, wake: true, home: def.home }) });
823
- if (run.startedRuntime) js.lastLaunchedAt = now.toISOString();
693
+ if (run.startedHarness) js.lastLaunchedAt = now.toISOString();
824
694
  if (!existsSync(def.home)) releaseJobLock(ws, id);
825
695
  js.lastRun = { ...(js.lastRun || {}), ...run, outcome: run.action, completedPending: run.action === "delivered", pendingSince: js.pendingWake.scheduledFor, startedAt: js.lastRun?.startedAt || now.toISOString() };
826
696
  if (!run.pending) delete js.pendingWake;
@@ -832,12 +702,8 @@ export function tickWorkspace(ws, { now = new Date(), io, reg, wsList, dryRun =
832
702
  if (js.lastAttemptedMinute === key && !only) continue;
833
703
  if (!due) continue;
834
704
  if (dryRun) {
835
- let admission;
836
- if (!js.attempt && !jobLockInfo(ws, id) && (def.kind === "wake" || liveLocks(wsAll) < bound)) {
837
- try { admitExecution(def, io, { createIntent: false }); } catch (error) { admission = error; }
838
- }
839
- const reason = js.attempt ? "an earlier launch has no recorded result; run oats schedule reconcile" : jobLockInfo(ws, id) && def.kind !== "wake" ? "still running" : liveLocks(wsAll) >= bound && def.kind !== "wake" ? "host busy" : admission ? `admission blocked (${admission.code || "error"}): ${admission.message}` : undefined;
840
- record(id, reason ? (admission ? "blocked" : "skipped") : "due", reason ? { reason, ...(admission ? { errorCode: admission.code } : {}) } : {});
705
+ const reason = js.attempt ? "an earlier launch has no recorded result; run oats schedule reconcile" : jobLockInfo(ws, id) && def.kind !== "wake" ? "still running" : liveLocks(wsAll) >= bound && def.kind !== "wake" ? "host busy" : undefined;
706
+ record(id, reason ? "skipped" : "due", reason ? { reason } : {});
841
707
  continue;
842
708
  }
843
709
  js.lastAttemptedMinute = key;
@@ -845,9 +711,7 @@ export function tickWorkspace(ws, { now = new Date(), io, reg, wsList, dryRun =
845
711
  let invalid;
846
712
  try { validateAttemptExecution(js.attempt, jobLockInfo(ws, id)); } catch (error) { invalid = error; }
847
713
  record(id, invalid ? "invalid" : "skipped", { reason: invalid ? `admitted attempt is invalid (${invalid.code || "error"}): ${invalid.message}` : "an earlier launch has no recorded result; run oats schedule reconcile", ...(invalid ? { errorCode: invalid.code } : {}) });
848
- if (!js.attempt.execution || receiptMatchesExecution(js.lastRun, js.attempt.execution)) {
849
- js.lastRun = { ...(js.lastRun || {}), outcome: "unknown", error: invalid ? `invalid admitted attempt: ${invalid.message}` : "launch attempt without a recorded result", ...(invalid ? { errorCode: invalid.code } : {}), scheduledFor: js.attempt.scheduledFor };
850
- }
714
+ js.lastRun = { ...(js.lastRun || {}), outcome: "unknown", error: invalid ? `invalid admitted attempt: ${invalid.message}` : "launch attempt without a recorded result", ...(invalid ? { errorCode: invalid.code } : {}), scheduledFor: js.attempt.scheduledFor };
851
715
  continue;
852
716
  }
853
717
  if (def.kind === "wake") {
@@ -855,7 +719,7 @@ export function tickWorkspace(ws, { now = new Date(), io, reg, wsList, dryRun =
855
719
  // one does, and the slot is held (the job lock) until that home ends.
856
720
  const holdsSlot = !!jobLockInfo(ws, id);
857
721
  const run = performWake(def, io, { startIfStopped: true, canStart: holdsSlot || liveLocks(wsAll) < bound, reserve: () => holdsSlot || acquireJobLock(ws, id, { scheduledFor: minute.toISOString(), wake: true, home: def.home }) });
858
- if (run.startedRuntime) js.lastLaunchedAt = now.toISOString();
722
+ if (run.startedHarness) js.lastLaunchedAt = now.toISOString();
859
723
  if (!existsSync(def.home)) releaseJobLock(ws, id);
860
724
  if (run.pending) js.pendingWake = { scheduledFor: minute.toISOString() }; else delete js.pendingWake;
861
725
  js.lastRun = { scheduledFor: minute.toISOString(), startedAt: now.toISOString(), home: def.home, ...run, outcome: run.action, ...(run.instance ? { instance: run.instance } : {}) };
@@ -864,26 +728,18 @@ export function tickWorkspace(ws, { now = new Date(), io, reg, wsList, dryRun =
864
728
  }
865
729
  if (jobLockInfo(ws, id)) { record(id, "skipped", { reason: "still running" }); continue; }
866
730
  if (liveLocks(wsAll) >= bound) { record(id, "skipped", { reason: "host busy" }); continue; }
867
- let execution;
868
- try { execution = admitExecution(def, io); }
869
- catch (error) {
870
- js.lastRun = { scheduledFor: minute.toISOString(), startedAt: now.toISOString(), kind: def.kind, launched: false, outcome: "blocked", error: error.message, errorCode: error.code || "E_SCHEDULE_ADMISSION" };
871
- record(id, "blocked", { error: error.message, errorCode: error.code || "E_SCHEDULE_ADMISSION" });
872
- continue;
873
- }
874
- // Persist the admitted authority before even reserving the launch slot.
875
- // A crash in the following gap leaves an inspectable unresolved attempt,
876
- // never an ownerless lock whose intended software cannot be recovered.
877
- js.attempt = { ...(execution ? { schemaVersion: SCHEDULE_ATTEMPT_VERSION } : {}), scheduledFor: minute.toISOString(), startedAt: now.toISOString(), wallClock: new Date().toISOString(), ...(execution ? { execution } : {}) };
731
+ // Persist the attempt before even reserving the launch slot. A crash in
732
+ // the following gap leaves an inspectable unresolved attempt, never an
733
+ // ownerless lock.
734
+ js.attempt = { scheduledFor: minute.toISOString(), startedAt: now.toISOString(), wallClock: new Date().toISOString() };
878
735
  writeState(ws, st);
879
- if (!acquireJobLock(ws, id, { scheduledFor: minute.toISOString(), ...(execution ? { executionId: execution.executionId } : {}) })) {
736
+ if (!acquireJobLock(ws, id, { scheduledFor: minute.toISOString() })) {
880
737
  delete js.attempt; writeState(ws, st); record(id, "skipped", { reason: "still running" }); continue;
881
738
  }
882
739
  js.lastLaunchedAt = now.toISOString();
883
740
  writeState(ws, st);
884
741
  let run;
885
- const admitted = execution ? { ...def, cwd: execution.target.cwd, argv: execution.target.argv } : def;
886
- try { run = def.kind === "spawn" ? launchSpawn(ws, def, minute, io) : launchCommand(ws, def.kind === "operation" ? operationAsCommand(def) : admitted, io); }
742
+ try { run = def.kind === "spawn" ? launchSpawn(ws, def, minute, io) : launchCommand(ws, def.kind === "operation" ? operationAsCommand(def) : def, io); }
887
743
  catch (e) {
888
744
  // spawnInstance compensates its own failures, but its rollback can be
889
745
  // INCOMPLETE (a pane it could not stop, a quarantined home it kept):
@@ -893,13 +749,13 @@ export function tickWorkspace(ws, { now = new Date(), io, reg, wsList, dryRun =
893
749
  }
894
750
  if (run.unconfirmed) {
895
751
  // Side effects unconfirmed: keep the attempt and the slot; reconcile decides.
896
- js.lastRun = { scheduledFor: minute.toISOString(), startedAt: now.toISOString(), ...(execution ? { execution } : {}), ...run, outcome: "unknown" };
752
+ js.lastRun = { scheduledFor: minute.toISOString(), startedAt: now.toISOString(), ...run, outcome: "unknown" };
897
753
  record(id, "unknown", { error: run.error });
898
754
  continue;
899
755
  }
900
756
  delete js.attempt;
901
757
  const outcome = run.launched ? "launched" : "launch-failed";
902
- js.lastRun = { scheduledFor: minute.toISOString(), startedAt: now.toISOString(), ...(execution ? { execution } : {}), ...run, outcome };
758
+ js.lastRun = { scheduledFor: minute.toISOString(), startedAt: now.toISOString(), ...run, outcome };
903
759
  if (!run.launched || !run.home) releaseJobLock(ws, id);
904
760
  record(id, outcome, { ...(run.error ? { error: run.error } : {}), ...(run.instance ? { instance: run.instance } : {}) });
905
761
  }
@@ -972,27 +828,25 @@ export function reconcile(ws, id, { io, now = new Date(), clear = false } = {})
972
828
  return withHostLock(() => {
973
829
  const st = readState(ws);
974
830
  const js = st.jobs[id] || (st.jobs[id] = {});
975
- const attempt = js.attempt || (js.lastRun?.outcome === "unknown" ? { ...(js.lastRun.execution ? { schemaVersion: SCHEDULE_ATTEMPT_VERSION } : {}), scheduledFor: js.lastRun.scheduledFor, startedAt: js.lastRun.startedAt, ...(js.lastRun.execution ? { execution: js.lastRun.execution } : {}) } : undefined);
831
+ const attempt = js.attempt || (js.lastRun?.outcome === "unknown" ? { scheduledFor: js.lastRun.scheduledFor, startedAt: js.lastRun.startedAt, ...(js.lastRun.execution !== undefined ? { execution: js.lastRun.execution } : {}) } : undefined);
976
832
  if (!attempt) return { schedule: describe(ws, id, io), reconciled: "nothing to reconcile" };
977
833
  try { validateAttemptExecution(attempt, jobLockInfo(ws, id)); }
978
- catch (error) { throw scheduleError("E_SCHEDULE_INVALID", `admitted execution capsule is invalid: ${error.message}`); }
979
- const attemptKind = attempt.execution?.action.kind || def.kind;
834
+ catch (error) { throw scheduleError("E_SCHEDULE_INVALID", error.message); }
835
+ const attemptKind = def.kind;
980
836
  let adopted, ambiguous;
981
837
  if (attemptKind === "spawn" && attempt.scheduledFor) {
982
838
  const purpose = `${def.purpose || def.id}-${purposeSuffix(new Date(attempt.scheduledFor))}`;
983
- const homes = findHomesInScope(ws, `${def.agent}-${purpose}`);
839
+ // Only a home of THIS schedule's soul: instance names are deployment-wide
840
+ // (0.26.0), so another soul's instance can never be this run's — a name it
841
+ // holds made this run's spawn derive a suffixed name instead.
842
+ const homes = findHomesInScope(ws, `${def.agent}-${purpose}`).filter((h) => {
843
+ try { const agent = JSON.parse(readFileSync(join(h, "instance.json"), "utf8")).agent; return typeof agent !== "string" || agent === def.agent; } catch { return true; }
844
+ });
984
845
  if (homes.length === 1) adopted = { instance: `${def.agent}-${purpose}`, home: homes[0] };
985
846
  else if (homes.length > 1) ambiguous = `${homes.length} homes named ${def.agent}-${purpose}: ${homes.join(", ")}`;
986
847
  } else if (attemptKind === "command" || attemptKind === "operation") {
987
848
  let named = [];
988
- if (attempt.execution) {
989
- // Only this admission's receipt may establish effects. Never borrow the
990
- // previous run's name or consult current-config roster discovery.
991
- if (receiptMatchesExecution(js.lastRun, attempt.execution) && typeof js.lastRun.home === "string" && isAbsolute(js.lastRun.home)) {
992
- const home = resolve(js.lastRun.home), meta = readJson(join(home, "instance.json"), null);
993
- if (meta?.instance === js.lastRun.instance && typeof meta.home === "string" && resolve(meta.home) === home) named = [{ instance: meta.instance, home }];
994
- }
995
- } else if (js.lastRun?.instance) named = findHomesInScope(ws, js.lastRun.instance).map((home) => ({ instance: js.lastRun.instance, home }));
849
+ if (js.lastRun?.instance) named = findHomesInScope(ws, js.lastRun.instance).map((home) => ({ instance: js.lastRun.instance, home }));
996
850
  if (named.length === 1) adopted = named[0];
997
851
  else if (named.length > 1) ambiguous = `${named.length} homes named ${js.lastRun.instance}: ${named.map((c) => c.home).join(", ")}`;
998
852
  else if (!clear) return { schedule: describe(ws, id, io), reconciled: "unknown", remedy: `the command's effects cannot be proven from a matching admitted receipt; check the roster and the host by hand, then run oats schedule reconcile ${id} --clear to record launch-failed and free the slot, or oats schedule remove --force ${id}` };
@@ -1002,10 +856,10 @@ export function reconcile(ws, id, { io, now = new Date(), clear = false } = {})
1002
856
  delete js.attempt;
1003
857
  if (adopted) {
1004
858
  const seen = observeHome(adopted.home, io);
1005
- js.lastRun = { scheduledFor: attempt.scheduledFor, startedAt: js.lastRun?.startedAt || attempt.startedAt, kind: attemptKind, ...(attempt.execution ? { execution: attempt.execution } : {}), launched: true, ...adopted, outcome: seen.outcome, ...(seen.error ? { error: seen.error } : {}) };
1006
- if (seen.outcome !== "ended") { if (!jobLockInfo(ws, id)) acquireJobLock(ws, id, { scheduledFor: attempt.scheduledFor, reconciled: true, ...(attempt.execution ? { executionId: attempt.execution.executionId } : {}) }); } else releaseJobLock(ws, id);
859
+ js.lastRun = { scheduledFor: attempt.scheduledFor, startedAt: js.lastRun?.startedAt || attempt.startedAt, kind: attemptKind, launched: true, ...adopted, outcome: seen.outcome, ...(seen.error ? { error: seen.error } : {}) };
860
+ if (seen.outcome !== "ended") { if (!jobLockInfo(ws, id)) acquireJobLock(ws, id, { scheduledFor: attempt.scheduledFor, reconciled: true }); } else releaseJobLock(ws, id);
1007
861
  } else {
1008
- js.lastRun = { scheduledFor: attempt.scheduledFor, startedAt: js.lastRun?.startedAt || attempt.startedAt, kind: attemptKind, ...(attempt.execution ? { execution: attempt.execution } : {}), launched: false, outcome: "launch-failed", error: clear ? "cleared by the operator after checking by hand" : "no instance found in the scope's roster for the unrecorded attempt" };
862
+ js.lastRun = { scheduledFor: attempt.scheduledFor, startedAt: js.lastRun?.startedAt || attempt.startedAt, kind: attemptKind, launched: false, outcome: "launch-failed", error: clear ? "cleared by the operator after checking by hand" : "no instance found in the scope's roster for the unrecorded attempt" };
1009
863
  releaseJobLock(ws, id);
1010
864
  }
1011
865
  writeState(ws, st);
@@ -1021,7 +875,7 @@ function incarnationOfHome(home) { try { const m = JSON.parse(readFileSync(join(
1021
875
  * transcript: there is no reader behind this block. */
1022
876
  function sessionProvenanceOf(r) {
1023
877
  if (!r || typeof r !== "object") return null;
1024
- const delivery = r.startedRuntime ? "launched" : r.action === "delivered" || r.outcome === "delivered" || r.completedPending ? "delivered-active" : r.launched === true || r.instance ? "launched" : "none";
878
+ const delivery = r.startedHarness || r.startedRuntime ? "launched" : r.action === "delivered" || r.outcome === "delivered" || r.completedPending ? "delivered-active" : r.launched === true || r.instance ? "launched" : "none";
1025
879
  return { instance: typeof r.instance === "string" ? r.instance : null, home: typeof r.home === "string" ? r.home : null, incarnation: typeof r.incarnation === "string" ? r.incarnation : null, server: typeof r.server === "string" ? r.server : null, delivery };
1026
880
  }
1027
881
  /** Read-time projection of one job's stored history: bounded to
@@ -1049,7 +903,7 @@ export function describe(ws, id, io, { defs, st, now = new Date() } = {}) {
1049
903
  const js = st.jobs[id] || {};
1050
904
  let nextRun = null; try { nextRun = def.enabled ? nextRunAfter(def, now) : null; } catch { nextRun = null; }
1051
905
  const lock = jobLockInfo(ws, id);
1052
- const intent = js.attempt || (lock ? (js.lastRun?.execution ? { schemaVersion: SCHEDULE_ATTEMPT_VERSION, execution: js.lastRun.execution } : {}) : undefined);
906
+ const intent = js.attempt || (lock ? (js.lastRun?.execution !== undefined ? { execution: js.lastRun.execution } : {}) : undefined);
1053
907
  const { history, recentRuns } = projectHistory(js);
1054
908
  return { ...def, id, scope: ws, scheduleApi: 2, scheduleHistoryApi: SCHEDULE_HISTORY_API, executionStatus: scheduleExecutionStatus(def, intent, lock), nextRun, lastRun: js.lastRun ? { ...js.lastRun, runId: runIdOf(js.lastRun), session: sessionProvenanceOf(js.lastRun) } : null, history, recentRuns, running: !!lock, ...(js.attempt ? { attempt: js.attempt } : {}), ...(js.pendingWake ? { pendingWake: js.pendingWake } : {}) };
1055
909
  }
@@ -1067,8 +921,6 @@ export function addSchedule(ws, spec, io) {
1067
921
  return withScopeLock(ws, () => {
1068
922
  const defs = readDefinitions(ws);
1069
923
  if (defs.jobs[def.id]) throw scheduleError("E_SCHEDULE_EXISTS", `schedule ${def.id} already exists in ${ws}`);
1070
- if (defs.version === 2 && def.definitionVersion === undefined) throw scheduleError("migration-required", "version 2 schedule files require an explicit recurrence policy for new definitions");
1071
- if (def.definitionVersion === SCHEDULE_DEFINITION_VERSION) defs.version = 2;
1072
924
  const now = new Date().toISOString();
1073
925
  defs.jobs[def.id] = { ...def, createdAt: now, updatedAt: now };
1074
926
  writeDefinitions(ws, defs);
@@ -1084,8 +936,6 @@ export function updateSchedule(ws, id, spec, io) {
1084
936
  return withHostLock(() => withScopeLock(ws, () => {
1085
937
  const defs = readDefinitions(ws);
1086
938
  if (!defs.jobs[id]) throw scheduleError("E_SCHEDULE_UNKNOWN", `no schedule ${JSON.stringify(id)} in ${ws}`);
1087
- if (defs.jobs[id].definitionVersion !== undefined && def.definitionVersion === undefined) throw scheduleError("migration-required", "a versioned schedule definition cannot be downgraded to legacy execution");
1088
- if (def.definitionVersion === SCHEDULE_DEFINITION_VERSION) defs.version = 2;
1089
939
  const identity = (d) => JSON.stringify([d.kind, d.agent, d.agentsRoot, d.repo, d.purpose, d.home, d.cwd, d.argv, d.operation, d.definitionVersion, d.recurrencePolicy, d.execution, d.preparation]);
1090
940
  const busy = !!jobLockInfo(ws, id) || !!readState(ws).jobs[id]?.attempt;
1091
941
  if (busy && identity(defs.jobs[id]) !== identity(def)) throw scheduleError("E_SCHEDULE_RUNNING", `schedule ${id} is running or has an unresolved attempt; its execution identity, kind, target and captured policy cannot change until it ends (cron, tz and enabled can)`);
@@ -1119,16 +969,10 @@ export function removeSchedule(ws, id, { force = false } = {}) {
1119
969
  }));
1120
970
  }
1121
971
  /** Save the wake job for a freshly spawned instance (id wake-<instance>). */
1122
- export function saveWakeForHome(ws, { instance, home, wake, executionBinding, responsibleHuman }) {
972
+ export function saveWakeForHome(ws, { instance, home, wake }) {
1123
973
  const id = `wake-${instance}`.slice(0, 40).replace(/-+$/, "");
1124
974
  const spec = { id, enabled: wake.enabled === undefined ? true : wake.enabled, cron: wake.cron, tz: wake.tz, kind: "wake", home, message: wake.message };
1125
- if (executionBinding !== undefined || responsibleHuman !== undefined) {
1126
- if (executionBinding === undefined || responsibleHuman === undefined) throw scheduleError("needs-configuration", "captured wake creation needs both executionBinding and responsibleHuman");
1127
- const binding = validateExecutionBinding(executionBinding), actual = homeExecutionBinding(home);
1128
- if (binding.deployment !== actual.deployment || binding.resolution.id !== actual.resolution.id) throw scheduleError("E_SCHEDULE_INVALID", "spawn result executionBinding differs from the new home");
1129
- Object.assign(spec, { definitionVersion: SCHEDULE_DEFINITION_VERSION, recurrencePolicy: "capture", responsibleHuman });
1130
- }
1131
- return addSchedule(ws, spec); // Same whole-file version/policy gate as manual add.
975
+ return addSchedule(ws, spec);
1132
976
  }
1133
977
  /** Retirement hook: wake definitions bound to this home are removed (nothing is stopped). */
1134
978
  export function removeWakeForHome(ws, home) {