@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.
- package/README.md +8 -6
- package/bin/oats.mjs +576 -1714
- package/capabilities/oats-authoring/oats-package.json +2 -2
- package/capabilities/oats-authoring/oats.json +2 -2
- package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +46 -25
- package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +13 -6
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +334 -72
- package/capabilities/oats-aweb/injects/aweb.md +7 -2
- package/capabilities/oats-aweb/lib/binding-wire.mjs +107 -5
- package/capabilities/oats-aweb/lib/captured-native.mjs +1 -1
- package/capabilities/oats-aweb/lib/grant-custody.mjs +38 -0
- package/capabilities/oats-aweb/oats.json +14 -4
- package/capabilities/oats-jira/bin/oats-jira.mjs +4 -4
- package/capabilities/oats-jira/oats.json +2 -2
- package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +6 -3
- package/capabilities/oats-linear/bin/oats-linear-hook.mjs +6 -4
- package/capabilities/oats-linear/oats.json +2 -2
- package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +6 -0
- package/capabilities/oats-okf/bin/oats-okf.mjs +1 -1
- package/capabilities/oats-okf/lib/binding-wire.mjs +1 -1
- package/capabilities/oats-okf/lib/migration.mjs +2 -2
- package/capabilities/oats-okf/lib/sources.mjs +5 -4
- package/capabilities/oats-okf/lib/stores.mjs +40 -9
- package/capabilities/oats-okf/lib/worker.mjs +3 -3
- package/capabilities/oats-okf/oats.json +1 -1
- package/capabilities/oats-review/oats.json +3 -2
- package/docs/capabilities.md +218 -47
- package/docs/capability-manifest.schema.json +13 -4
- package/docs/configuration.md +17 -5
- package/docs/conventions.md +16 -26
- package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +1 -1
- package/docs/design/2026-09-13-knowledge-and-memory-direction.md +3 -3
- package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +2 -2
- package/docs/design/2026-09-15-portable-souls-handoff.md +2 -2
- package/docs/design/2026-09-15-portable-souls-implementation.md +1 -1
- package/docs/design/2026-09-20-redesign-program-board.md +2 -2
- package/docs/design/2026-09-23-workspace-module-contracts.md +1 -1
- package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +1 -1
- package/docs/design/2026-09-24-desktop-phase-f-boundary.md +54 -10
- package/docs/design/2026-09-24-phase-d-plan.md +77 -0
- package/docs/design/2026-09-25-teams-contract.md +226 -0
- package/docs/design/README.md +3 -3
- package/docs/design/launch-configurations.md +20 -16
- package/docs/design/operations-contract.md +27 -10
- package/docs/desktop-cli-api.md +546 -264
- package/docs/desktop-instance-start.md +1 -1
- package/docs/desktop.md +7 -13
- package/docs/execution-targets.md +16 -18
- package/docs/first-team.md +14 -17
- package/docs/implementation.md +28 -59
- package/docs/integrations.md +88 -32
- package/docs/knowledge-capability-authoring.md +1 -1
- package/docs/knowledge-reference/package-craft.md +10 -8
- package/docs/knowledge-theory.md +1 -1
- package/docs/knowledge.md +10 -11
- package/docs/layers.md +16 -17
- package/docs/oats-local.schema.json +29 -1
- package/docs/oats-membership.schema.json +5 -3
- package/docs/oats-package.schema.json +2 -2
- package/docs/oats-workspace.schema.json +1 -1
- package/docs/{official-marketplace.md → official-catalog.md} +15 -16
- package/docs/packages.md +75 -52
- package/docs/release-notes/v0.22.0.md +1 -1
- package/docs/release-notes/v0.23.1.md +1 -1
- package/docs/release-notes/v0.25.9.md +23 -0
- package/docs/release-notes/v0.26.0.md +670 -0
- package/docs/schedules.md +48 -126
- package/docs/soul.schema.json +11 -4
- package/docs/souls-and-instances.md +56 -43
- package/docs/workspaces.md +80 -58
- package/injects/instance-boundary.md +1 -1
- package/injects/work-attached.md +1 -1
- package/injects/work-workspace.md +2 -2
- package/lib/{portable-files.mjs → bounded-read.mjs} +6 -6
- package/lib/{portable-values.mjs → canonical-json.mjs} +3 -12
- package/lib/capability-contract.mjs +110 -0
- package/lib/config-data.mjs +2 -2
- package/lib/core.mjs +700 -4824
- package/lib/digest.mjs +12 -0
- package/lib/instance-inspect.mjs +396 -0
- package/lib/instance-lifecycle.mjs +3 -4
- package/lib/instance-resolution.mjs +212 -26
- package/lib/instruction-composition.mjs +0 -20
- package/lib/materialize.mjs +6 -4
- package/lib/operator-dispatch.mjs +33 -13
- package/lib/packages.mjs +25 -190
- package/lib/provider-binding.mjs +4 -2
- package/lib/provider-reasons.mjs +3 -68
- package/lib/resolve.mjs +204 -68
- package/lib/schedule.mjs +97 -272
- package/lib/servers.mjs +13 -13
- package/lib/{portable-shape.mjs → shape.mjs} +4 -3
- package/lib/tree-copy.mjs +44 -0
- package/lib/workspace.mjs +125 -20
- package/package-catalog.json +7 -7
- package/package.json +1 -1
- package/skills/integration-authoring/SKILL.md +48 -40
- package/skills/oats-getting-started/SKILL.md +105 -110
- package/skills/oats-support/SKILL.md +2 -2
- package/skills/soul-craft/SKILL.md +13 -6
- package/bin/oats-pi-sdk-host.mjs +0 -17
- package/docs/2026-09-03-architecture-proposal.md +0 -642
- package/docs/artifact-approvals.schema.json +0 -7
- package/docs/captured-invocation-context.schema.json +0 -7
- package/docs/captured-resolution.schema.json +0 -7
- package/docs/design/package-engine-contract.md +0 -813
- package/docs/design/package-runtime-api.md +0 -588
- package/docs/desktop-succession.md +0 -57
- package/docs/execution-capsule.schema.json +0 -108
- package/docs/first-team-demo.md +0 -92
- package/docs/knowledge-migration.md +0 -147
- package/docs/migration-from-oas.md +0 -103
- package/docs/oats-config.schema.json +0 -172
- package/docs/oats-lock-v3.schema.json +0 -7
- package/docs/oats-lock.schema.json +0 -175
- package/docs/operating-team-migration.md +0 -470
- package/docs/portable.schema.json +0 -2512
- package/docs/provider-check-input.schema.json +0 -7
- package/docs/rebuild-to-v2.md +0 -511
- package/docs/workspace-adoption.md +0 -74
- package/injects/framework-workspace.md +0 -7
- package/injects/local-soul.md +0 -19
- package/injects/oats-portable.md +0 -20
- package/injects/oats.md +0 -11
- package/injects/portable-instance-boundary.md +0 -39
- package/injects/portable-work-directory.md +0 -29
- package/lib/artifact-approvals.mjs +0 -120
- package/lib/artifact-tree.mjs +0 -141
- package/lib/capability-artifacts.mjs +0 -179
- package/lib/capability-execution.mjs +0 -15
- package/lib/capability-inputs.mjs +0 -39
- package/lib/capability-provenance.mjs +0 -231
- package/lib/captured-action-shape.mjs +0 -21
- package/lib/captured-admission-shape.mjs +0 -20
- package/lib/captured-binding-file.mjs +0 -36
- package/lib/captured-dispatch.mjs +0 -66
- package/lib/captured-instance-index.mjs +0 -277
- package/lib/captured-invocation-context.mjs +0 -130
- package/lib/captured-launch-request.mjs +0 -66
- package/lib/captured-operation-process.mjs +0 -15
- package/lib/captured-pi-custody.mjs +0 -29
- package/lib/captured-pi-host.mjs +0 -167
- package/lib/captured-pi-outcome.mjs +0 -172
- package/lib/captured-resolutions.mjs +0 -275
- package/lib/captured-scaffold.mjs +0 -87
- package/lib/captured-selector.mjs +0 -28
- package/lib/captured-session-backend.mjs +0 -52
- package/lib/captured-source-receipt-file.mjs +0 -72
- package/lib/helper-injection-policy.mjs +0 -104
- package/lib/legacy-lock-codec.mjs +0 -106
- package/lib/manifest-settings.mjs +0 -84
- package/lib/package-closure.mjs +0 -48
- package/lib/package-materialization.mjs +0 -83
- package/lib/pi-sdk-host.mjs +0 -229
- package/lib/portable-artifacts.mjs +0 -115
- package/lib/portable-choices.mjs +0 -82
- package/lib/portable-composition.mjs +0 -136
- package/lib/portable-digest.mjs +0 -105
- package/lib/portable-identity.mjs +0 -40
- package/lib/portable-lock.mjs +0 -117
- package/lib/portable-onboarding-request.mjs +0 -49
- package/lib/portable-onboarding.mjs +0 -256
- package/lib/portable-package-preparation.mjs +0 -188
- package/lib/portable-policy.mjs +0 -44
- package/lib/portable-soul.mjs +0 -42
- package/lib/portable-state.mjs +0 -80
- package/lib/prepare-composition.mjs +0 -170
- package/lib/prepared-bindings.mjs +0 -92
- package/lib/prepared-resources.mjs +0 -127
- package/lib/provider-binding-broker.mjs +0 -65
- package/lib/provider-binding-wire.mjs +0 -116
- package/lib/readiness.mjs +0 -225
- package/lib/repository-observation.mjs +0 -226
- package/lib/resolution-shape.mjs +0 -393
- package/lib/schedule-capsule.mjs +0 -206
- package/lib/soul-constraints.mjs +0 -40
- package/lib/source-projection.mjs +0 -84
- package/lib/source-spec.mjs +0 -189
- package/lib/workspace-definition.mjs +0 -126
- package/lib/workspace-discovery.mjs +0 -146
- package/skills/oats/SKILL.md +0 -162
- package/skills/oats-config/SKILL.md +0 -164
- package/skills/oats-packages/SKILL.md +0 -184
- package/skills/oats-portable/SKILL.md +0 -115
- 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.
|
|
12
|
-
|
|
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'],['
|
|
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-
|
|
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.
|
|
4
|
+
"version": "1.13.1",
|
|
5
5
|
"compatibility": {
|
|
6
|
-
"oats": ">=0.
|
|
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
|
|
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
|
|
31
|
-
: project ? `project ${project} (site unset — ask your human
|
|
32
|
-
: `your deployment's Jira (site/project not configured — ask your human
|
|
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
|
|
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.
|
|
5
|
-
"compatibility": { "oats": ">=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
|
|
33
|
-
|
|
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.
|
|
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:
|
|
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
|
-
:
|
|
23
|
+
: `team unset — ask your human to set ${TEAM_HOMES}`;
|
|
22
24
|
const warnings = [];
|
|
23
|
-
if (!team) warnings.push(
|
|
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
|
|
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.
|
|
5
|
-
"compatibility": { "oats": ">=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
|
|
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
|
-
|
|
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'
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"capability": "oats.review",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
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"],
|