@awebai/oats 0.25.8 → 0.26.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 (185) hide show
  1. package/README.md +8 -6
  2. package/bin/oats.mjs +576 -1714
  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 +334 -72
  8. package/capabilities/oats-aweb/injects/aweb.md +7 -2
  9. package/capabilities/oats-aweb/lib/binding-wire.mjs +107 -5
  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 +14 -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-okf/bin/oats-okf.mjs +1 -1
  20. package/capabilities/oats-okf/lib/binding-wire.mjs +1 -1
  21. package/capabilities/oats-okf/lib/migration.mjs +2 -2
  22. package/capabilities/oats-okf/lib/sources.mjs +5 -4
  23. package/capabilities/oats-okf/lib/stores.mjs +40 -9
  24. package/capabilities/oats-okf/lib/worker.mjs +3 -3
  25. package/capabilities/oats-okf/oats.json +1 -1
  26. package/capabilities/oats-review/oats.json +3 -2
  27. package/docs/capabilities.md +218 -47
  28. package/docs/capability-manifest.schema.json +13 -4
  29. package/docs/configuration.md +17 -5
  30. package/docs/conventions.md +16 -26
  31. package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +1 -1
  32. package/docs/design/2026-09-13-knowledge-and-memory-direction.md +3 -3
  33. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +2 -2
  34. package/docs/design/2026-09-15-portable-souls-handoff.md +2 -2
  35. package/docs/design/2026-09-15-portable-souls-implementation.md +1 -1
  36. package/docs/design/2026-09-20-redesign-program-board.md +2 -2
  37. package/docs/design/2026-09-23-workspace-module-contracts.md +1 -1
  38. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +1 -1
  39. package/docs/design/2026-09-24-desktop-phase-f-boundary.md +54 -10
  40. package/docs/design/2026-09-24-phase-d-plan.md +77 -0
  41. package/docs/design/2026-09-25-teams-contract.md +226 -0
  42. package/docs/design/README.md +3 -3
  43. package/docs/design/launch-configurations.md +20 -16
  44. package/docs/design/operations-contract.md +27 -10
  45. package/docs/desktop-cli-api.md +546 -264
  46. package/docs/desktop-instance-start.md +1 -1
  47. package/docs/desktop.md +7 -13
  48. package/docs/execution-targets.md +16 -18
  49. package/docs/first-team.md +14 -17
  50. package/docs/implementation.md +28 -59
  51. package/docs/integrations.md +88 -32
  52. package/docs/knowledge-capability-authoring.md +1 -1
  53. package/docs/knowledge-reference/package-craft.md +10 -8
  54. package/docs/knowledge-theory.md +1 -1
  55. package/docs/knowledge.md +10 -11
  56. package/docs/layers.md +16 -17
  57. package/docs/oats-local.schema.json +29 -1
  58. package/docs/oats-membership.schema.json +5 -3
  59. package/docs/oats-package.schema.json +2 -2
  60. package/docs/oats-workspace.schema.json +1 -1
  61. package/docs/{official-marketplace.md → official-catalog.md} +15 -16
  62. package/docs/packages.md +75 -52
  63. package/docs/release-notes/v0.22.0.md +1 -1
  64. package/docs/release-notes/v0.23.1.md +1 -1
  65. package/docs/release-notes/v0.25.9.md +23 -0
  66. package/docs/release-notes/v0.26.0.md +670 -0
  67. package/docs/schedules.md +48 -126
  68. package/docs/soul.schema.json +11 -4
  69. package/docs/souls-and-instances.md +56 -43
  70. package/docs/workspaces.md +80 -58
  71. package/injects/instance-boundary.md +1 -1
  72. package/injects/work-attached.md +1 -1
  73. package/injects/work-workspace.md +2 -2
  74. package/lib/{portable-files.mjs → bounded-read.mjs} +6 -6
  75. package/lib/{portable-values.mjs → canonical-json.mjs} +3 -12
  76. package/lib/capability-contract.mjs +110 -0
  77. package/lib/config-data.mjs +2 -2
  78. package/lib/core.mjs +700 -4824
  79. package/lib/digest.mjs +12 -0
  80. package/lib/instance-inspect.mjs +396 -0
  81. package/lib/instance-lifecycle.mjs +3 -4
  82. package/lib/instance-resolution.mjs +212 -26
  83. package/lib/instruction-composition.mjs +0 -20
  84. package/lib/materialize.mjs +6 -4
  85. package/lib/operator-dispatch.mjs +33 -13
  86. package/lib/packages.mjs +25 -190
  87. package/lib/provider-binding.mjs +4 -2
  88. package/lib/provider-reasons.mjs +3 -68
  89. package/lib/resolve.mjs +204 -68
  90. package/lib/schedule.mjs +97 -272
  91. package/lib/servers.mjs +13 -13
  92. package/lib/{portable-shape.mjs → shape.mjs} +4 -3
  93. package/lib/tree-copy.mjs +44 -0
  94. package/lib/workspace.mjs +125 -20
  95. package/package-catalog.json +7 -7
  96. package/package.json +1 -1
  97. package/skills/integration-authoring/SKILL.md +48 -40
  98. package/skills/oats-getting-started/SKILL.md +105 -110
  99. package/skills/oats-support/SKILL.md +2 -2
  100. package/skills/soul-craft/SKILL.md +13 -6
  101. package/bin/oats-pi-sdk-host.mjs +0 -17
  102. package/docs/2026-09-03-architecture-proposal.md +0 -642
  103. package/docs/artifact-approvals.schema.json +0 -7
  104. package/docs/captured-invocation-context.schema.json +0 -7
  105. package/docs/captured-resolution.schema.json +0 -7
  106. package/docs/design/package-engine-contract.md +0 -813
  107. package/docs/design/package-runtime-api.md +0 -588
  108. package/docs/desktop-succession.md +0 -57
  109. package/docs/execution-capsule.schema.json +0 -108
  110. package/docs/first-team-demo.md +0 -92
  111. package/docs/knowledge-migration.md +0 -147
  112. package/docs/migration-from-oas.md +0 -103
  113. package/docs/oats-config.schema.json +0 -172
  114. package/docs/oats-lock-v3.schema.json +0 -7
  115. package/docs/oats-lock.schema.json +0 -175
  116. package/docs/operating-team-migration.md +0 -470
  117. package/docs/portable.schema.json +0 -2512
  118. package/docs/provider-check-input.schema.json +0 -7
  119. package/docs/rebuild-to-v2.md +0 -511
  120. package/docs/workspace-adoption.md +0 -74
  121. package/injects/framework-workspace.md +0 -7
  122. package/injects/local-soul.md +0 -19
  123. package/injects/oats-portable.md +0 -20
  124. package/injects/oats.md +0 -11
  125. package/injects/portable-instance-boundary.md +0 -39
  126. package/injects/portable-work-directory.md +0 -29
  127. package/lib/artifact-approvals.mjs +0 -120
  128. package/lib/artifact-tree.mjs +0 -141
  129. package/lib/capability-artifacts.mjs +0 -179
  130. package/lib/capability-execution.mjs +0 -15
  131. package/lib/capability-inputs.mjs +0 -39
  132. package/lib/capability-provenance.mjs +0 -231
  133. package/lib/captured-action-shape.mjs +0 -21
  134. package/lib/captured-admission-shape.mjs +0 -20
  135. package/lib/captured-binding-file.mjs +0 -36
  136. package/lib/captured-dispatch.mjs +0 -66
  137. package/lib/captured-instance-index.mjs +0 -277
  138. package/lib/captured-invocation-context.mjs +0 -130
  139. package/lib/captured-launch-request.mjs +0 -66
  140. package/lib/captured-operation-process.mjs +0 -15
  141. package/lib/captured-pi-custody.mjs +0 -29
  142. package/lib/captured-pi-host.mjs +0 -167
  143. package/lib/captured-pi-outcome.mjs +0 -172
  144. package/lib/captured-resolutions.mjs +0 -275
  145. package/lib/captured-scaffold.mjs +0 -87
  146. package/lib/captured-selector.mjs +0 -28
  147. package/lib/captured-session-backend.mjs +0 -52
  148. package/lib/captured-source-receipt-file.mjs +0 -72
  149. package/lib/helper-injection-policy.mjs +0 -104
  150. package/lib/legacy-lock-codec.mjs +0 -106
  151. package/lib/manifest-settings.mjs +0 -84
  152. package/lib/package-closure.mjs +0 -48
  153. package/lib/package-materialization.mjs +0 -83
  154. package/lib/pi-sdk-host.mjs +0 -229
  155. package/lib/portable-artifacts.mjs +0 -115
  156. package/lib/portable-choices.mjs +0 -82
  157. package/lib/portable-composition.mjs +0 -136
  158. package/lib/portable-digest.mjs +0 -105
  159. package/lib/portable-identity.mjs +0 -40
  160. package/lib/portable-lock.mjs +0 -117
  161. package/lib/portable-onboarding-request.mjs +0 -49
  162. package/lib/portable-onboarding.mjs +0 -256
  163. package/lib/portable-package-preparation.mjs +0 -188
  164. package/lib/portable-policy.mjs +0 -44
  165. package/lib/portable-soul.mjs +0 -42
  166. package/lib/portable-state.mjs +0 -80
  167. package/lib/prepare-composition.mjs +0 -170
  168. package/lib/prepared-bindings.mjs +0 -92
  169. package/lib/prepared-resources.mjs +0 -127
  170. package/lib/provider-binding-broker.mjs +0 -65
  171. package/lib/provider-binding-wire.mjs +0 -116
  172. package/lib/readiness.mjs +0 -225
  173. package/lib/repository-observation.mjs +0 -226
  174. package/lib/resolution-shape.mjs +0 -393
  175. package/lib/schedule-capsule.mjs +0 -206
  176. package/lib/soul-constraints.mjs +0 -40
  177. package/lib/source-projection.mjs +0 -84
  178. package/lib/source-spec.mjs +0 -189
  179. package/lib/workspace-definition.mjs +0 -126
  180. package/lib/workspace-discovery.mjs +0 -146
  181. package/skills/oats/SKILL.md +0 -162
  182. package/skills/oats-config/SKILL.md +0 -164
  183. package/skills/oats-packages/SKILL.md +0 -184
  184. package/skills/oats-portable/SKILL.md +0 -115
  185. package/skills/oats-portable-artifacts/SKILL.md +0 -63
@@ -8,8 +8,13 @@ Some instances serve a resident global identity through an expiring session
8
8
  grant instead of holding their own root keys. In that mode, your TASK.md and
9
9
  `instance.json` identify the resident alias, scopes, and expiry; root keys are
10
10
  not in your home, and mint/revoke/join/identity-lifecycle commands are not yours
11
- to run. If `aw mail` or `aw chat` says the grant expired or was revoked, stop
12
- messaging and report the condition to your coordinator/human.
11
+ to run. Your grant home is attached to the resident's custody service; if a
12
+ message you sent shows unverified at the receiver, the attachment is missing:
13
+ report it, do not retry. If `aw mail` or `aw chat` reports a terminal grant condition —
14
+ `grant_expired`, `grant_revoked`, `grant_subject_inactive`,
15
+ `grant_issuer_revoked`, or `grant_freshness_unavailable` — report the exact
16
+ condition to your coordinator/human and stop using messaging. The host restarts
17
+ you with a fresh grant when renewal is available.
13
18
 
14
19
  **Load the skills at the right moments — do not work from memory:**
15
20
 
@@ -1,5 +1,9 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
3
+ import { isAbsolute, join, resolve } from 'node:path';
1
4
  import { TextDecoder } from 'node:util';
2
5
  import { assessCapturedSessionReadiness } from './session-readiness.mjs';
6
+ import { custodyPreflight } from './grant-custody.mjs';
3
7
  import {
4
8
  MESSAGING_CONTRACT,
5
9
  MESSAGING_CONTRACT_VERSION,
@@ -76,17 +80,24 @@ export function parseBindingJson(bytes,limits=BINDING_WIRE_LIMITS) {
76
80
  let text;try{text=new TextDecoder('utf-8',{fatal:true}).decode(bytes);}catch{wireError('invalid-binding');}
77
81
  return new StrictJsonParser(text,limits).parse();
78
82
  }
79
- function settings(value) {
83
+ function settings(value,{phase}={}) {
80
84
  if(!obj(value)) wireError('invalid-binding');
81
- if(Object.hasOwn(value,'identity')) wireError('provider-not-qualified');
82
- keys(value,['delivery'],[]);
85
+ if(phase!=='check' && Object.hasOwn(value,'identity')) wireError('provider-not-qualified');
86
+ keys(value,phase==='check'?['delivery','team','root','roots','identity','residents']:['delivery','team','root','roots'],[]);
83
87
  if(value.delivery!==undefined && !['channel','session'].includes(value.delivery)) wireError('needs-configuration');
88
+ if(value.team!==undefined && (typeof value.team!=='string' || !value.team.trim())) wireError('needs-configuration');
89
+ if(value.root!==undefined && (typeof value.root!=='string' || !value.root.trim())) wireError('needs-configuration');
90
+ if(value.roots!==undefined && !obj(value.roots)) wireError('needs-configuration');
91
+ if(obj(value.roots)) for(const [team,root] of Object.entries(value.roots)) if(!team || typeof root!=='string' || !root.trim()) wireError('needs-configuration');
92
+ if(value.identity!==undefined && !obj(value.identity)) wireError('needs-configuration');
93
+ if(value.residents!==undefined && !obj(value.residents)) wireError('needs-configuration');
94
+ if(obj(value.residents)) for(const [name,custody] of Object.entries(value.residents)) if(!name || typeof custody!=='string' || !custody.trim()) wireError('needs-configuration');
84
95
  return value;
85
96
  }
86
97
  function request(value,phase) {
87
98
  keys(value,['schemaVersion','phase','slot','capability','settings','input'],['schemaVersion','phase','slot','capability','settings','input']);
88
99
  if(value.schemaVersion!==1 || value.phase!==phase || value.slot!==SLOT || value.capability!==CAPABILITY) wireError('invalid-binding');
89
- settings(value.settings);return value;
100
+ settings(value.settings,{phase});return value;
90
101
  }
91
102
  function declaration(value) {
92
103
  keys(value,['kind','value','origin','origins'],['kind','value','origin','origins']);
@@ -142,14 +153,105 @@ function binding(value) {
142
153
  return value;
143
154
  }
144
155
  function checkResult(message) {return {status:'needs-configuration',problems:[{code:'needs-configuration',message}]};}
156
+ function checkProblems(problems) {return problems.length?{status:'needs-configuration',problems}:null;}
157
+ function workspaceReadinessContext(value) {
158
+ keys(value,['kind','workspace','deployment','soul','team','instance','home'],['kind','workspace','deployment','soul']);
159
+ if(value.kind!=='workspace' || typeof value.workspace!=='string' || !value.workspace.trim() || typeof value.deployment!=='string' || !value.deployment.trim() || typeof value.soul!=='string' || !value.soul.trim()) wireError('invalid-binding');
160
+ if(value.team!==null && value.team!==undefined && (typeof value.team!=='string' || !value.team.trim())) wireError('invalid-binding');
161
+ if(value.instance!==null && value.instance!==undefined && (typeof value.instance!=='string' || !value.instance.trim())) wireError('invalid-binding');
162
+ if(value.home!==null && value.home!==undefined && (typeof value.home!=='string' || !value.home.trim())) wireError('invalid-binding');
163
+ return value;
164
+ }
165
+ function yamlScalar(text,key){const m=String(text).match(new RegExp(`^${key}:\\s*["']?([^"'\\n#]+)["']?\\s*$`,'m'));return m?m[1].trim():undefined;}
166
+ export const CUSTODY_ATTACH_MIN = '1.36.3';
167
+ export function grantYamlCustodySocket(text) {
168
+ const lines=String(text??'').split(/\r?\n/);let inCustody=false,baseIndent=0;
169
+ for(const line of lines) {
170
+ const custody=/^(\s*)custody:\s*(?:#.*)?$/.exec(line);if(custody){inCustody=true;baseIndent=custody[1].length;continue;}
171
+ if(inCustody){const ind=/^(\s*)/.exec(line)?.[1].length||0;if(line.trim()&&ind<=baseIndent)inCustody=false;const socket=/^\s*socket_path:\s*["']?([^"'\n#]+)["']?\s*$/.exec(line);if(socket)return socket[1].trim();}
172
+ }
173
+ return undefined;
174
+ }
175
+ function newestGrantHome(home) {
176
+ if(typeof home!=='string'||!home.trim()) return null;
177
+ try {
178
+ const dirs=readdirSync(home).filter((name)=>name==='.aweb-identity'||/^\.aweb-identity-\d+$/.test(name)).map((name)=>join(home,name)).filter((p)=>{try{return statSync(p).isDirectory();}catch{return false;}}).map((p)=>{try{return {path:p,mtime:statSync(p).mtimeMs};}catch{return {path:p,mtime:0};}}).sort((a,b)=>b.mtime-a.mtime||b.path.localeCompare(a.path));
179
+ return dirs[0]?.path||null;
180
+ } catch {return null;}
181
+ }
182
+ function grantAttachmentProblem(home) {
183
+ const grantHome=newestGrantHome(home);
184
+ if(!grantHome) return null;
185
+ const grantYaml=join(grantHome,'grant.yaml');
186
+ if(!existsSync(grantYaml)) return null;
187
+ let text;try{text=readFileSync(grantYaml,'utf8');}catch{return null;}
188
+ if(grantYamlCustodySocket(text)) return null;
189
+ const id=yamlScalar(text,'grant_id')||'<unknown>';
190
+ return {code:'custody',message:`grant ${id} is not attached to custody; retire and respawn on aw >= ${CUSTODY_ATTACH_MIN}`};
191
+ }
192
+ function activeTeamAt(root){try{return yamlScalar(readFileSync(join(resolve(root),'.aw','teams.yaml'),'utf8'),'active_team')||yamlScalar(readFileSync(join(resolve(root),'.aw','teams.yaml'),'utf8'),'active');}catch{return undefined;}}
193
+ function teamFromSettings(settings,candidate,{env=process.env}={}) {
194
+ const configured=typeof settings.team==='string' && settings.team.trim()?settings.team.trim():(env.OATS_TEAM_ID || env.OATS_TEAM_NAME || undefined);
195
+ if(configured || env.OATS_TEAM_LABEL) return configured;
196
+ return candidate?.root && isAbsolute(candidate.root) ? activeTeamAt(candidate.root) : undefined;
197
+ }
198
+ function classicEnv(env=process.env) {return !!env.OATS_TEAM_SCOPE && !(env.OATS_WORKSPACE_KEY || env.OATS_WORKSPACE_NAME || env.OATS_TEAM_LABEL);}
199
+ function rootCandidate(settings,team,{deployment,env=process.env}={}) {
200
+ const roots=obj(settings.roots)?settings.roots:{};
201
+ if(team && typeof roots[team]==='string' && roots[team].trim()) return {root:roots[team].trim(),key:`settings.oats.aweb.roots[${JSON.stringify(team)}]`,declared:true};
202
+ if(typeof settings.root==='string' && settings.root.trim()) return {root:settings.root.trim(),key:'settings.oats.aweb.root',declared:true};
203
+ const candidates=classicEnv(env)?[env.OATS_TEAM_SCOPE,env.OATS_WORKSPACE].filter(Boolean):[env.OATS_WORKSPACE || deployment || env.OATS_TEAM_SCOPE || process.cwd()];
204
+ for(const root of candidates) if(isAbsolute(root) && existsSync(join(resolve(root),'.aw'))) return {root,key:'settings.oats.aweb.root',declared:false};
205
+ return {root:candidates[0] || process.cwd(),key:'settings.oats.aweb.root',declared:false};
206
+ }
207
+ function readinessDetails(settings,{deployment,env=process.env}={}) {
208
+ const initialTeam=typeof settings.team==='string' && settings.team.trim()?settings.team.trim():(env.OATS_TEAM_ID || env.OATS_TEAM_NAME || undefined);
209
+ const candidate=rootCandidate(settings,initialTeam,{deployment,env}),team=teamFromSettings(settings,candidate,{env}),problems=[];
210
+ if(!candidate.root || !isAbsolute(candidate.root) || !existsSync(join(resolve(candidate.root),'.aw'))) problems.push({code:'needs-configuration',message:`no messaging root at ${candidate.root?resolve(candidate.root):process.cwd()}: run oats aweb setup there or set ${candidate.key}`});
211
+ if(!team) problems.push({code:'needs-configuration',message:'no team: set messaging.byTeam.<label>.team in the workspace file or settings.oats.aweb.team'});
212
+ return {team,candidate,result:checkProblems(problems) || {status:'ready',problems:[]}};
213
+ }
214
+ function readinessFromSettings(settings,options) {return readinessDetails(settings,options).result;}
215
+ function runAw(argv,cwd,{unsetEnv=[],timeout=60000}={}) {
216
+ const env={...process.env};for(const name of unsetEnv) delete env[name];
217
+ try {return execFileSync(argv[0],argv.slice(1),{cwd,env,encoding:'utf8',stdio:['ignore','pipe','pipe'],timeout}).trim();}
218
+ catch(e) {throw new Error(`${argv.slice(0,3).join(' ')} failed${e.status===undefined?'':` (exit ${e.status})`}`);}
219
+ }
220
+ function workspaceReadinessPhase(req) {
221
+ const ctx=workspaceReadinessContext(req.input.context);
222
+ if(!obj(req.input.action) || req.input.action.kind!=='readiness') wireError('invalid-binding');
223
+ const details=readinessDetails(req.settings,{deployment:ctx.deployment}),problems=[...details.result.problems],warnings=[];
224
+ const identity=obj(req.settings.identity)?req.settings.identity:{},mode=identity.mode===undefined || identity.mode===null || identity.mode===''?'local':String(identity.mode);
225
+ if(mode==='global') {
226
+ const grantProblem=grantAttachmentProblem(ctx.home);if(grantProblem) problems.push(grantProblem);
227
+ const resident=typeof identity.resident==='string' && identity.resident.trim()?identity.resident.trim():undefined;
228
+ const residents=obj(req.settings.residents)?req.settings.residents:{};
229
+ const custody=resident?residents[resident]:undefined;
230
+ if(!resident) problems.push({code:'custody',message:'identity.mode "global" requires identity.resident; set oats-local.yaml settings.oats.aweb.residents.<name> to the absolute custody directory for that resident identity'});
231
+ else if(typeof custody!=='string' || !isAbsolute(custody) || !existsSync(join(custody,'.aw','identity.yaml'))) problems.push({code:'custody',message:`identity.mode "global" resident ${JSON.stringify(resident)} is not resolvable; set oats-local.yaml settings.oats.aweb.residents.${resident} to an absolute custody directory whose .aw/identity.yaml exists`});
232
+ else if(details.team) {
233
+ try {
234
+ const preflight=custodyPreflight({custody,resident,team:details.team,e2eeRequired:identity.e2ee!==false,fatalOnError:false,runAw:(argv,cwd,options={})=>runAw(argv,cwd,{...options,timeout:20000})});
235
+ for(const message of preflight.warnings) warnings.push({code:'e2ee-disabled',message});
236
+ }
237
+ catch(e) {problems.push({code:'custody',message:e.message});}
238
+ }
239
+ }
240
+ const result=checkProblems(problems) || {status:'ready',problems:[]};
241
+ return {...result,warnings};
242
+ }
145
243
  function checkPhase(req) {
146
- keys(req.input,['binding','context','action','invocation'],['binding','context','action']);
244
+ keys(req.input,['binding','context','action','invocation'],['context','action']);
147
245
  if(!obj(req.input.action) || typeof req.input.action.kind!=='string') wireError('invalid-binding');
246
+ if(!Object.hasOwn(req.input,'binding')) return workspaceReadinessPhase(req);
148
247
  const current=validateAwebBinding(binding(req.input.binding)),selectedContext=context(req.input.context,{request:true});
149
248
  if(!same(selectedContext,current.payload.context)) wireError('invalid-binding');
150
249
  const invocation=Object.hasOwn(req.input,'invocation')?validateAwebInvocationContext(req.input.invocation,current,{context:req.input.context,action:req.input.action}):null;
151
250
  if(['command','hook','operation'].includes(req.input.action.kind) && (!invocation || invocation.instance===null || invocation.intent===null)) return checkResult('an admitted captured instance intent is required for execution');
152
251
  if(current.payload.privateTeam===null) return checkResult('an explicit private-team binding is required');
252
+ const hostReady=readinessFromSettings(req.settings);
253
+ if(hostReady.status!=='ready') return hostReady;
254
+ if(!invocation) return hostReady;
153
255
  // Read-only public kernel observations, not an account/grant attestation.
154
256
  // Native setup is performed only by the separately admitted execution path.
155
257
  return assessCapturedSessionReadiness({binding:current,invocation,settings:req.settings});
@@ -26,7 +26,7 @@ export function runCapturedNative({selected,event,settings,run,env=process.env})
26
26
  const refusal=(code,message)=>({exitCode:1,output:{...(meta?{meta}:{}),status:'needs-configuration',problems:[{code,message}],warning:`oats-aweb: ${message}; retain the home and any recorded partial effects`}});
27
27
  try{
28
28
  assertCurrent();
29
- if(!/^[a-z0-9][a-z0-9._-]{0,127}$/i.test(name)||!team)fail('needs-configuration','exact native alias and private-team selection are required');
29
+ if(!/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(name)||!team)fail('needs-configuration','exact native alias and private-team selection are required');
30
30
  if(settings.delivery!=='session')fail('needs-configuration','captured delivery must be session');
31
31
  if(binding.payload.wider.length)fail('needs-configuration','selected wider memberships need their explicitly qualified native setup; they were not omitted');
32
32
  // A global override may select a different actor even with an exact cwd.
@@ -0,0 +1,38 @@
1
+ export function parseAwJson(text, what) {
2
+ const trimmed = String(text ?? '').trim();
3
+ if (!trimmed) throw new Error(`${what} returned no JSON result`);
4
+ try { return JSON.parse(trimmed); } catch { /* may have progress before JSON */ }
5
+ const lines = trimmed.split(/\r?\n/);
6
+ for (let i = 0; i < lines.length; i++) {
7
+ if (!lines[i].trimStart().startsWith('{')) continue;
8
+ try { return JSON.parse(lines.slice(i).join('\n')); } catch { /* keep looking */ }
9
+ }
10
+ throw new Error(`${what} returned no JSON result`);
11
+ }
12
+
13
+ export function custodyPreflight({ custody, resident, team, e2eeRequired = true, runAw, fatalOnError = true, fatal }) {
14
+ const failNow = (message) => { if (fatalOnError && typeof fatal === 'function') fatal(message); throw new Error(message); };
15
+ let status;
16
+ try { status = parseAwJson(runAw(['aw', 'custody', 'status', '--json'], custody, { unsetEnv: ['AWEB_IDENTITY_HOME'] }), 'aw custody status'); }
17
+ catch (e) { failNow(`custody preflight failed for ${resident}: aw custody status --json could not run (${e.message || e}); start aw custody serve for ${resident}`); }
18
+ const state = String(status.status || 'unknown');
19
+ const firstError = Array.isArray(status.errors) && status.errors.length ? status.errors[0] : undefined;
20
+ const firstErrorCode = typeof firstError === 'string' ? firstError : firstError?.code;
21
+ const code = firstErrorCode ? ` error=${firstErrorCode}` : '';
22
+ const fail = (why) => failNow(`custody preflight failed for ${resident}: status=${state}${code}; ${why}; start aw custody serve for ${resident}`);
23
+ if (state !== 'running') fail('custody service is not running');
24
+ const teamRow = (Array.isArray(status.teams) ? status.teams : []).find((t) => t && (t.team_id || t.id) === team);
25
+ if (!teamRow) fail(`team ${team} is not present in custody status`);
26
+ if (teamRow.ready !== true) fail(`team ${team} is not ready in custody status`);
27
+ if (teamRow.certificate_present === false) fail(`team ${team} certificate is not present in custody status`);
28
+ if (teamRow.grant_status_endpoint_ready !== undefined && teamRow.grant_status_endpoint_ready !== true) fail(`the aweb server serving team ${team} does not provide grant status yet; hosted grants wait for that deployment`);
29
+ if (status.keys?.signing_ready !== true) fail('keys.signing_ready is false');
30
+ const ops = new Set(Array.isArray(status.ops) ? status.ops.map(String) : []);
31
+ const requiredOps = ['sign_plain_message.v1', ...(e2eeRequired ? ['unwrap_e2ee_message.v1', 'create_e2ee_envelope.v1'] : [])];
32
+ const missingOps = requiredOps.filter((op) => !ops.has(op));
33
+ if (missingOps.length) fail(`required custody operations are missing: ${missingOps.join(', ')}`);
34
+ if (e2eeRequired && status.keys?.encryption_ready !== true) fail('keys.encryption_ready is false');
35
+ const warnings = [];
36
+ if (!e2eeRequired && status.keys?.encryption_ready !== true) warnings.push('E2E encryption is disabled for this grant and custody encryption is not ready; encrypted mail/chat will not be available in this session.');
37
+ return { status, warnings };
38
+ }
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "capability": "oats.aweb",
3
3
  "command": "aweb",
4
- "version": "1.12.0",
4
+ "version": "1.13.1",
5
5
  "compatibility": {
6
- "oats": ">=0.24.4"
6
+ "oats": ">=0.25.6"
7
7
  },
8
8
  "layer": "messaging",
9
9
  "description": "Messaging layer via aweb: per-instance team identities + native aw mail/chat skills + cross-machine team roster.",
@@ -121,7 +121,8 @@
121
121
  "command": "bin/oats-aweb.mjs spawn",
122
122
  "required": true
123
123
  },
124
- "retire": "bin/oats-aweb.mjs retire"
124
+ "retire": "bin/oats-aweb.mjs retire",
125
+ "launch": "bin/oats-aweb.mjs launch"
125
126
  },
126
127
  "environment": [
127
128
  "AWEB_DELIVERY",
@@ -139,13 +140,22 @@
139
140
  "team": {
140
141
  "description": "Target aweb team id for identity lifecycle. In workspace v2 spawns this payload value wins over OATS_TEAM_ID/OATS_TEAM_NAME; if both are set and differ the hook warns and uses this setting."
141
142
  },
143
+ "root": {
144
+ "hostOnly": true,
145
+ "description": "Host-owned absolute directory whose .aw is the aweb minting root. Put this only in oats-local.yaml settings.oats.aweb.root; use oats aweb setup to initialize it."
146
+ },
147
+ "roots": {
148
+ "hostOnly": true,
149
+ "description": "Host-owned map of aweb team id to absolute directory whose .aw is that team's minting root, for deployments that mint into several teams. Put this only in oats-local.yaml settings.oats.aweb.roots."
150
+ },
142
151
  "identity": {
143
152
  "default": {
144
153
  "mode": "local"
145
154
  },
146
- "description": "Identity selection. mode=local (default) mints an instance-local team identity or uses the existing identity.source retained-seat flow. mode=global acts as a named resident identity through a session grant; requires identity.resident and optional scopes/ttl."
155
+ "description": "Identity selection. mode=local (default) mints an instance-local team identity or uses the existing identity.source retained-seat flow. mode=global acts as a named resident identity through a session grant; requires identity.resident. Optional global fields: profile (normal|reviewer), scopes (explicit concrete grant scope list, wins over profile), ttl, e2ee (default true), renew (off|launch, default off)."
147
156
  },
148
157
  "residents": {
158
+ "hostOnly": true,
149
159
  "description": "Host-owned map for global mode: resident name to absolute custody directory whose .aw holds the resident root keys and team certificate. Put this only in oats-local.yaml settings.oats.aweb.residents; committed workspace or soul files must never carry custody paths."
150
160
  }
151
161
  },
@@ -27,13 +27,13 @@ if (event === "spawn") {
27
27
  const site = settings.site;
28
28
  const project = settings.project;
29
29
  const where = site && project ? `project ${project} on ${site}`
30
- : site ? `site ${site} (project unset — ask your human or check oats doctor)`
31
- : project ? `project ${project} (site unset — ask your human or check oats doctor)`
32
- : `your deployment's Jira (site/project not configured — ask your human, or set capabilities.oats.jira.<target>.settings in oats-config.yaml)`;
30
+ : site ? `site ${site} (project unset — ask your human)`
31
+ : project ? `project ${project} (site unset — ask your human)`
32
+ : `your deployment's Jira (site/project not configured — ask your human to set tasks: { site, project } in the soul's soul.yaml, or settings.oats.jira.{site,project} in the deployment's oats-local.yaml)`;
33
33
  out({
34
34
  meta: { label, ...(site ? { site } : {}), ...(project ? { project } : {}) },
35
35
  brief: `Tasks: Jira — ${where}. Your Jira identity is the label "${label}" (never the assignee field). Load the jira-tasks skill before touching tickets.`,
36
- ...(site && project ? {} : { warning: `oats-jira: settings incomplete (site: ${site || "unset"}, project: ${project || "unset"}) — set capabilities.oats.jira.<target>.settings.{site,project} in oats-config.yaml` }),
36
+ ...(site && project ? {} : { warning: `oats-jira: settings incomplete (site: ${site || "unset"}, project: ${project || "unset"}) — set tasks: { site, project } in the soul's soul.yaml, or settings.oats.jira.{site,project} in the deployment's oats-local.yaml` }),
37
37
  });
38
38
  } else {
39
39
  warn(`unknown event "${event}" (expected spawn)`);
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "capability": "oats.jira",
3
3
  "command": "jira",
4
- "version": "1.0.0",
5
- "compatibility": { "oats": ">=0.19.0" },
4
+ "version": "1.0.1",
5
+ "compatibility": { "oats": ">=0.26.0" },
6
6
  "layer": "tasks",
7
7
  "description": "Tasks layer via Jira: acli-based epic/story/task protocol, agent roster in epic descriptions, label-based agent identity.",
8
8
  "requires": [
@@ -29,12 +29,15 @@ tracking and the agent roster. Agents do not know about projects — you know
29
29
 
30
30
  ## Site and project (from your deployment, never hardcoded)
31
31
 
32
- Your Jira **site** and **project key** come from the deployment's OATS config
33
- (`capabilities.oats.jira.<target>.settings: { site, project }`). Find them, in order:
32
+ Your Jira **site** and **project key** come from the tasks payload OATS merged
33
+ for this instance: the soul's `soul.yaml` `tasks: { site, project }`, the
34
+ deployment's `oats-local.yaml` `settings.oats.jira.{site,project}`, or a
35
+ spawn's `--provider oats.jira key=value`. Find them, in order:
34
36
 
35
37
  1. Your `TASK.md` briefing — the spawn hook writes a
36
38
  `Tasks: Jira — project <KEY> on <site>` line.
37
- 2. `oats doctor --json` from your work tree (the tasks layer's settings).
39
+ 2. `./instance.json` in your instance home — `providers["oats.jira"]` is the
40
+ merged payload this instance received.
38
41
  3. Ask your human.
39
42
 
40
43
  Below, `<PROJECT>` means that project key. If site or project are unset,
@@ -10,23 +10,25 @@ if (event !== "spawn") output({ warning: `oats-linear: unknown event "${event}"
10
10
 
11
11
  let settings = {};
12
12
  try { settings = JSON.parse(process.env.OATS_SETTINGS || "{}"); }
13
- catch { output({ warning: "oats-linear: integrations.linear.settings is not valid JSON" }); }
13
+ catch { output({ warning: "oats-linear: the tasks settings payload (OATS_SETTINGS) is not valid JSON" }); }
14
14
 
15
15
  const instance = process.env.OATS_INSTANCE || "unknown-instance";
16
16
  const team = settings.team;
17
17
  const project = settings.project;
18
18
  const label = `agent-${instance}`;
19
+ // Where team lives under the workspace model (0.26): the soul's tasks payload or this machine's settings.
20
+ const TEAM_HOMES = "tasks: { team } in the soul's soul.yaml, or settings.oats.linear.team in the deployment's oats-local.yaml";
19
21
  const target = team
20
22
  ? `team ${team}${project ? `, default project ${project}` : ""}`
21
- : "team unset — ask your human, or set integrations.linear.settings.team in oats-config.yaml";
23
+ : `team unset — ask your human to set ${TEAM_HOMES}`;
22
24
  const warnings = [];
23
- if (!team) warnings.push("settings.team is unset");
25
+ if (!team) warnings.push(`settings.team is unset (set ${TEAM_HOMES})`);
24
26
  if (!process.env.LINEAR_API_KEY) warnings.push("LINEAR_API_KEY is not in the spawn environment");
25
27
 
26
28
  output({
27
29
  meta: { label, ...(team ? { team } : {}), ...(project ? { project } : {}) },
28
30
  brief: `Tasks: Linear — ${target}. Your agent identity is label "${label}"; keep the human assignee unchanged. Load the linear-tasks skill before touching issues.`,
29
31
  ...(warnings.length ? {
30
- warning: `oats-linear: ${warnings.join("; ")} — see capabilities/oats-linear/README.md`,
32
+ warning: `oats-linear: ${warnings.join("; ")} — see the linear-tasks skill`,
31
33
  } : {}),
32
34
  });
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "capability": "oats.linear",
3
3
  "command": "linear",
4
- "version": "1.0.0",
5
- "compatibility": { "oats": ">=0.19.0" },
4
+ "version": "1.0.1",
5
+ "compatibility": { "oats": ">=0.26.0" },
6
6
  "layer": "tasks",
7
7
  "description": "Tasks layer via Linear: JSON-first GraphQL commands, project/issue/sub-issue workflow, label-based agent identity.",
8
8
  "requires": [],
@@ -23,6 +23,12 @@ Get the target from the `Tasks: Linear` line in your `TASK.md` briefing:
23
23
  - **project** is an optional deployment default. Do not invent one when unset.
24
24
  - **alias** is your exact OATS instance name; your label is `agent-<alias>`.
25
25
 
26
+ team and project come from the tasks payload OATS merged for this instance —
27
+ the soul's `soul.yaml` `tasks: { team, project }`, the deployment's
28
+ `oats-local.yaml` `settings.oats.linear.*`, or a spawn's `--provider` — and
29
+ `./instance.json` in your instance home records it as `providers["oats.linear"]`.
30
+ If team is unset, stop and ask your human to set it there.
31
+
26
32
  Before the first operation, run:
27
33
 
28
34
  ```bash
@@ -175,7 +175,7 @@ else {
175
175
  } else if(event==='unlock') result=unlock(resolve(flags.lock),flags.token);
176
176
  else fail('E_USAGE',`unknown command ${event}; see --help`);
177
177
  answer=hook?result:{schemaVersion:1,ok:true,result};
178
- } catch(e) {exit=1;answer=hook?{meta:{...(event==='retire'?{retired:false,reason:e.message}:{})},warning:`oats-okf: ${e.message}`}:{schemaVersion:1,ok:false,error:{code:e.code || 'E_OKF',message:e.message}};}
178
+ } catch(e) {const code=e.code || 'E_OKF';exit=1;answer=hook?{meta:{...(event==='retire'?{retired:false,reason:e.message}:{})},warning:`oats-okf ${code}: ${e.message}`}:{schemaVersion:1,ok:false,error:{code,message:e.message}};}
179
179
  // Let Node drain the pipe; no process.exit after a possibly large view.
180
180
  process.stdout.write(JSON.stringify(answer)+'\n');process.exitCode=exit;
181
181
  }
@@ -291,7 +291,7 @@ function checkPhase(req) {
291
291
  if(gitBases.length) scratch=fs.mkdtempSync(join(fs.realpathSync(tmpdir()),'oats-okf-binding-check-'));
292
292
  for(const [alias,base] of Object.entries(bindings.bases)) {
293
293
  stage=base.kind==='directory'?'validate':'stage';
294
- accepted[alias]=(base.kind==='directory'?validateBase(base.path,base):stageBase(base,join(scratch,alias))).meta;
294
+ accepted[alias]=(base.kind==='directory'?validateBase(base.path,base):stageBase(base,join(scratch,alias),{alias})).meta;
295
295
  }
296
296
  stage='runtime';
297
297
  checkKnowledgeRuntime({rendered:runtime,accepted});
@@ -33,7 +33,7 @@ export function migrate(bindings,{legacy,alias,node,output}) {
33
33
  // Stage the accepted base before any record exists: a base that cannot be
34
34
  // read leaves nothing behind. From the record on, a failure is recorded in
35
35
  // it rather than left as an unexplained directory.
36
- const stage=stageBase(base,output);
36
+ const stage=stageBase(base,output,{alias});
37
37
  fs.mkdirSync(bindings.stateDir,{recursive:true,mode:0o700});
38
38
  const id=randomUUID();const dir=join(bindings.stateDir,'migrations',id);fs.mkdirSync(dir,{recursive:true,mode:0o700});
39
39
  save(join(dir,'legacy.json'),original); // byte-preserving backup BEFORE any delivery
@@ -106,7 +106,7 @@ export function cutoverMigration(file,soul) {
106
106
  try {
107
107
  for(const alias of new Set([...decl.owns,...decl.reads].map(r=>splitRef(r)[0]))) {
108
108
  if(!Object.hasOwn(bindings.bases,alias)) fail('E_CONFIG',`unresolved base: ${alias}`);
109
- const staged=stageBase(bindings.bases[alias],join(scratch,alias));
109
+ const staged=stageBase(bindings.bases[alias],join(scratch,alias),{alias});
110
110
  accepted[alias]=staged.meta;
111
111
  if(alias===m.alias) {
112
112
  if(hash(staged.meta.nodes[m.node] || null)!==hash(frozen)) fail('E_OWNER','accepted migration ownership/path differs from delivered node');
@@ -101,7 +101,7 @@ export function views(bindings, decl, target) {
101
101
  for(const [alias,base] of Object.entries(bindings.bases)) {
102
102
  const scratch=fs.mkdtempSync(join(bindings.stateDir,'read-'));
103
103
  try {
104
- const staged=stageBase(base,join(scratch,'base'));
104
+ const staged=stageBase(base,join(scratch,'base'),{alias});
105
105
  all[alias]=staged.meta;
106
106
  const path=`bases/${alias}`;
107
107
  materialize(join(pending,path),staged.files);
@@ -192,7 +192,8 @@ export function register(home) {
192
192
  if(fs.existsSync(join(home,'knowledge'))) fail('E_VIEW','unregistered knowledge view exists; preserve it and inspect before registering');
193
193
  if(['.okf-harvest-record.json','.okf-harvest-record.next.json'].some(p=>fs.existsSync(join(home,p))) && !fs.existsSync(join(home,'.okf-v1-migration.json'))) fail('E_MIGRATION','legacy source watermarks require explicit oats okf migrate --source-home PATH before v2 registration; no cursor is silently trusted');
194
194
  const meta=fs.existsSync(join(home,'instance.json'))?readJSON(join(home,'instance.json')):{};
195
- const soul=fs.realpathSync(process.env.OATS_SOUL || join(home,'soul'));
195
+ if(!process.env.OATS_SOUL) fail('E_OATS_SOUL_MISSING','OATS_SOUL is not set; oats.okf hooks and commands run only under the OATS kernel');
196
+ const soul=fs.realpathSync(process.env.OATS_SOUL);
196
197
  const work=fs.existsSync(join(home,'work'))?fs.realpathSync(join(home,'work')):join(home,'work');
197
198
  const decl=declaration(soul);
198
199
  const soulId=process.env.OATS_SOUL_ID || null;
@@ -204,7 +205,6 @@ export function register(home) {
204
205
  if(!agent || !instance) fail('E_SOURCE','source instance/agent required');
205
206
  fs.mkdirSync(bindings.stateDir,{recursive:true,mode:0o700});
206
207
  const ownersFile=join(bindings.stateDir,'owners.json');
207
- pinOwner(ownersFile,decl.owner,{id:soulId,soulName:agent,path:soul});
208
208
  const id=randomUUID(); const dir=join(bindings.stateDir,'sources',id);
209
209
  // Copy only the role document, never instance.json wholesale, launch recipes,
210
210
  // environment, credentials, source worktree, or third-party message stores.
@@ -217,6 +217,7 @@ export function register(home) {
217
217
  fs.mkdirSync(dir,{recursive:true,mode:0o700});
218
218
  try {
219
219
  source.acceptedView=views(bindings,decl,pending);
220
+ pinOwner(ownersFile,decl.owner,{id:soulId,soulName:agent,path:soul});
220
221
  source.acceptedNodes=Object.fromEntries(Object.entries(source.acceptedView).map(([alias,r])=>[alias,r.nodes]));
221
222
  save(file,source);
222
223
  save(join(dir,'status.json'),{version:1,captured:{notes:[],threads:{},inputs:[]},processed:[],delivered:{},accepted:{},retired:false,auto:true,activeRun:null});
@@ -245,7 +246,7 @@ export function pinOwner(ownersFile,owner,{id,soulName,path}) {
245
246
  if(!obj(owners)) fail('E_OWNER','invalid owner registry');
246
247
  const prior=Object.hasOwn(owners,owner)?owners[owner]:undefined;
247
248
  const samePath=typeof prior==='string' && new RegExp(`/agents/${soulName.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')}/(soul|souls/[^/]+)$`).test(prior);
248
- if(prior!==undefined && prior!==value && !(id && samePath)) fail('E_OWNER','stable owner ID already identifies a different soul in this state namespace');
249
+ if(prior!==undefined && prior!==value && !(id && samePath)) fail('E_OWNER',`stable owner ID already identifies a different soul in this state namespace: existing soul ${prior}; new soul ${value}. Remedies: retire the existing registration first, or use a fresh state directory.`);
249
250
  if(prior!==value) {owners[owner]=value;save(ownersFile,owners);}
250
251
  return value;
251
252
  });
@@ -9,6 +9,31 @@ const validator = fileURLToPath(new URL('../skills/okf/scripts/okf-validate.mjs'
9
9
  // transport subprocesses. Override even an explicitly supplied command env.
10
10
  const gitEnv = (env = cleanEnv()) => ({...env,GIT_NO_REPLACE_OBJECTS:'1'});
11
11
  export const git = (cwd,args,opts={}) => exec('git',['--no-replace-objects','-c','core.hooksPath=/dev/null','-c','protocol.ext.allow=never','-C',cwd,...args],{cwd,...opts,env:gitEnv(opts.env)});
12
+ function baseError(code,message,base,alias,step,reason) {throw Object.assign(new Error(message),{code,base:alias,repository:base.repository,step,reason});}
13
+ function baseRemedy(alias) {return `fix the binding for base alias "${alias}" in the bindings file, or remove the base from the bindings`;}
14
+ function classifyGitFailure(error) {
15
+ const text=String(error?.message || '');
16
+ if(error?.code==='ETIMEDOUT' || /ETIMEDOUT|timed out|timeout/i.test(text)) return 'timeout';
17
+ if(/authentication|permission denied|publickey|access denied|could not read Username|authorization|not authorized/i.test(text)) return 'auth';
18
+ if(/not found|repository .*not exist|does not exist|no such (file|repository)|couldn't find remote ref|Repository not found/i.test(text)) return 'not-found';
19
+ if(/could not resolve host|failed to connect|connection refused|network is unreachable|no route to host|proxy/i.test(text)) return 'network';
20
+ return 'unknown';
21
+ }
22
+ function unavailable(base,alias,step,error) {
23
+ const reason=classifyGitFailure(error),detail=reason==='unknown'?`; original Git failure: ${String(error?.message || 'unknown failure')}`:'';
24
+ const message=`Git base "${alias}" repository "${base.repository}" is required by the deployment's bindings, but ${step} failed (reason: ${reason}${detail}); ${baseRemedy(alias)}`;
25
+ baseError('E_BASE_UNAVAILABLE',message,base,alias,step,reason);
26
+ }
27
+ function requireNotShallow(base,alias,cwd) {
28
+ const shallow=git(cwd,['rev-parse','--is-shallow-repository']);
29
+ if(shallow==='true') baseError('E_BASE_SHALLOW',`Git base "${alias}" repository "${base.repository}" is required by the deployment's bindings, but the repository is shallow; oats.okf requires full accepted history before staging; ${baseRemedy(alias)}`,base,alias,'clone','shallow');
30
+ }
31
+ function preflightLocalRepository(base,alias) {
32
+ if(!base.repository.startsWith('/')) return;
33
+ if(!fs.existsSync(base.repository)) unavailable(base,alias,'clone',Object.assign(new Error('repository not found'),{code:'ENOENT'}));
34
+ try { requireNotShallow(base,alias,base.repository); }
35
+ catch(e) { if(e.code==='E_BASE_SHALLOW') throw e; unavailable(base,alias,'clone',e); }
36
+ }
12
37
  export const baseLock = b => `${b.path}.okf-lock`;
13
38
  export const journalPath = b => `${b.path}.okf-publication.json`;
14
39
  export function validateBase(root, base) {
@@ -53,9 +78,10 @@ function materializeGitObjects(base,dest,head) {
53
78
  writeBlob(dest,entry.oid,target,entry.mode==='100755'?0o755:0o644);
54
79
  }
55
80
  }
56
- function clone(base, dest, selectedHead) {
81
+ function clone(base, dest, selectedHead, { alias = base.id } = {}) {
57
82
  safePath(dest);
58
83
  if(fs.existsSync(dest)) fail('E_PATH',`staging destination exists: ${dest}`);
84
+ preflightLocalRepository(base,alias);
59
85
  fs.mkdirSync(dirname(dest),{recursive:true});
60
86
  // Fetch only what the store reads: the accepted branch, trees now and blobs
61
87
  // on demand. Only a remote that does not offer object filtering gets a plain
@@ -64,25 +90,30 @@ function clone(base, dest, selectedHead) {
64
90
  const cloneArgs=['clone','--no-hardlinks','--no-checkout','--single-branch','--branch',base.acceptedBranch];
65
91
  try { git(dirname(dest),[...cloneArgs,'--filter=blob:none','--',base.repository,dest],{timeout:gitTimeoutMs()}); }
66
92
  catch(e) {
67
- if(e.code!=='E_COMMAND' || !/filter/i.test(e.message)) throw e;
68
- fs.rmSync(dest,{recursive:true,force:true});
69
- git(dirname(dest),[...cloneArgs,'--',base.repository,dest],{timeout:gitTimeoutMs()});
93
+ if(e.code==='E_COMMAND' && /filter/i.test(e.message)) {
94
+ fs.rmSync(dest,{recursive:true,force:true});
95
+ try { git(dirname(dest),[...cloneArgs,'--',base.repository,dest],{timeout:gitTimeoutMs()}); }
96
+ catch(error) { unavailable(base,alias,'clone',error); }
97
+ } else unavailable(base,alias,'clone',e);
70
98
  }
71
- git(dest,['fetch','origin',`refs/heads/${base.acceptedBranch}`],{timeout:gitTimeoutMs()});
99
+ try { requireNotShallow(base,alias,dest); }
100
+ catch(e) { if(e.code==='E_BASE_SHALLOW') throw e; unavailable(base,alias,'clone',e); }
101
+ try { git(dest,['fetch','origin',`refs/heads/${base.acceptedBranch}`],{timeout:gitTimeoutMs()}); }
102
+ catch(e) { unavailable(base,alias,'fetch',e); }
72
103
  const head=selectedHead ?? git(dest,['rev-parse','FETCH_HEAD']);
73
- verifyRemote(base,dest);
74
- materializeGitObjects(base,dest,head);
104
+ try { requireNotShallow(base,alias,dest);verifyRemote(base,dest);materializeGitObjects(base,dest,head); }
105
+ catch(e) { if(['E_BASE_SHALLOW','E_OWNER','E_CONFIRM','E_PATH','E_BASE','E_VALIDATION'].includes(e.code)) throw e; unavailable(base,alias,'checkout',e); }
75
106
  // Reject a linked bundle even if Git happily checked the link out.
76
107
  safePath(join(dest,base.root));
77
108
  return head;
78
109
  }
79
- export function stageBase(base, dest) {
110
+ export function stageBase(base, dest, { alias = base.id } = {}) {
80
111
  safePath(dest);
81
112
  if(fs.existsSync(dest)) fail('E_PATH',`staging destination exists: ${dest}`);
82
113
  const acceptedPath=base.kind==='directory'?base.path:(base.repository.startsWith('/')?resolve(base.repository,base.root):null);
83
114
  if(acceptedPath && overlaps(acceptedPath,dest)) fail('E_PATH','stage overlaps accepted base');
84
115
  if(base.kind==='git') {
85
- const head=clone(base,dest); const root=join(dest,base.root);
116
+ const head=clone(base,dest,undefined,{alias}); const root=join(dest,base.root);
86
117
  const validated=validateBase(root,base);
87
118
  verifyPublicationTree(base,dest,head,head,validated.files);
88
119
  return { ...validated, head, root, checkout:dest };
@@ -142,7 +142,7 @@ function prepareWorker(source,run) {
142
142
  for(const [alias,base] of Object.entries(source.bindings.bases)) {
143
143
  if(run.settled?.includes(alias)) continue;
144
144
  const dest=join(work,'bases',alias);
145
- const staged=stageBase(base,dest);
145
+ const staged=stageBase(base,dest,{alias});
146
146
  const owned=source.decl.owns.map(splitRef).filter(([a])=>a===alias).map(([,n])=>n);
147
147
  for(const n of owned) if(staged.meta.nodes[n]?.owner!==source.owner || JSON.stringify(staged.meta.nodes[n])!==JSON.stringify(source.acceptedNodes[alias][n])) fail('E_OWNER','accepted ownership/path changed from frozen destination; explicit migration required');
148
148
  run.stages[alias]={root:staged.root,checkout:staged.checkout,head:staged.head,baseline:staged.files,digest:staged.digest,owned};persist(source,run);
@@ -235,7 +235,7 @@ export function complete(source,id,judgmentFile,opts={}) {
235
235
  if(base.kind==='git') verifyGitScope(base,s.checkout,s.head);
236
236
  const checkDir=fs.mkdtempSync(join(source.bindings.stateDir,'baseline-'));
237
237
  try {
238
- const current=stageBase(base,join(checkDir,'base'));
238
+ const current=stageBase(base,join(checkDir,'base'),{alias});
239
239
  if(current.digest!==s.digest || (base.kind==='git' && current.head!==s.head)) fail('E_BASELINE','accepted base changed; rejudge on fresh baseline');
240
240
  } finally {fs.rmSync(checkDir,{recursive:true,force:true});}
241
241
  const claims=judgment.outcomes.flatMap(o=>o.concepts).filter(c=>c.base===alias).map(c=>c.path);
@@ -310,7 +310,7 @@ export function retry(source,{run:id,rejudge=false,launch=false,adoptHome}={}) {
310
310
  const stages={...run.stages},outstanding=Object.keys(stages).filter(a=>!settled.includes(a));
311
311
  for(const alias of outstanding) {
312
312
  const base=source.bindings.bases[alias];
313
- const staged=stageBase(base,join(work,'rejudgments',attempt,'bases',alias));
313
+ const staged=stageBase(base,join(work,'rejudgments',attempt,'bases',alias),{alias});
314
314
  const owned=run.stages[alias].owned;
315
315
  for(const n of owned) if(staged.meta.nodes[n]?.owner!==source.owner || JSON.stringify(staged.meta.nodes[n])!==JSON.stringify(source.acceptedNodes[alias][n])) fail('E_OWNER','accepted ownership/path changed from frozen destination; explicit migration required');
316
316
  stages[alias]={root:staged.root,checkout:staged.checkout,head:staged.head,baseline:staged.files,digest:staged.digest,owned};
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "capability": "oats.okf",
3
3
  "command": "okf",
4
- "version": "2.1.4",
4
+ "version": "2.1.5",
5
5
  "compatibility": {
6
6
  "oats": ">=0.24.4"
7
7
  },
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "capability": "oats.review",
3
- "version": "1.2.0",
4
- "compatibility": { "oats": ">=0.19.0" },
3
+ "private": true,
4
+ "version": "1.2.1",
5
+ "compatibility": { "oats": ">=0.26.0" },
5
6
  "description": "Post-commit review discipline: a fresh reviewer agent (defined by this capability) reviews each new commit's diff, reports its verdict to its spawner over the deployment's messaging layer (or in its transcript when there is none), and retires — plus the shared developer delivery discipline.",
6
7
  "requires": [],
7
8
  "agents": ["agents/reviewer"],