yaaw-se 0.3.13 → 0.3.14-dev.39042534464.1

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.
@@ -28,8 +28,8 @@
28
28
  },
29
29
  {
30
30
  "path": "payload.json",
31
- "sha256": "11921054038944aad1e18d9818ca049d06157201ea0bf9684e59b29a0a25096f",
32
- "bytes": 57
31
+ "sha256": "cbc20c34ecb09bd67b21cb3714a5e54459cc7cbd306b708a67bfaca7e0563de5",
32
+ "bytes": 75
33
33
  },
34
34
  {
35
35
  "path": "skills/yaaw-create-spec/SKILL.md",
@@ -96,11 +96,6 @@
96
96
  "sha256": "6189616f6ddfb5992dcc22e562f2f2c061ce5f3202c09dfce0f5c9adef5bf42f",
97
97
  "bytes": 1247
98
98
  },
99
- {
100
- "path": "yaaw-core/system/engine/contracts.mjs",
101
- "sha256": "b9541c15dcbb842da1812876c862e51b9a4422fcead81b274c19a864bf0885b3",
102
- "bytes": 10180
103
- },
104
99
  {
105
100
  "path": "yaaw-core/system/engine/frontmatter.mjs",
106
101
  "sha256": "2c90097563a91c52e6f9c7748dac583e67967fd4a9748e393bf61d753594277e",
@@ -118,18 +113,18 @@
118
113
  },
119
114
  {
120
115
  "path": "yaaw-core/system/engine/routing.mjs",
121
- "sha256": "7e051c636325d1c94b1a31ba409406e6ce9b5918857a3d39bf5fbd3b14c36365",
122
- "bytes": 13728
116
+ "sha256": "1e20ba87e76878aac6047d0ee62dbc55a24ee20a6a2551d36a803e892d32f170",
117
+ "bytes": 13250
123
118
  },
124
119
  {
125
120
  "path": "yaaw-core/system/engine/runtime.mjs",
126
- "sha256": "605bef2be22d18cbeaa04cf0b2bbdec13a23ab5c3db857706e29bd309c25df63",
127
- "bytes": 14049
121
+ "sha256": "dc30505164950e81ad6343b4c8d04cfe3604bfc84c8d4aec36eff424c614e614",
122
+ "bytes": 12293
128
123
  },
129
124
  {
130
125
  "path": "yaaw-core/system/kernel.yaml",
131
- "sha256": "706475307535f2e1c8042aab932e78cbada4af36460391d4fa29ad5067fb012c",
132
- "bytes": 22539
126
+ "sha256": "7e93d186484bebeed95ac0ddc68f489b198fbf94a6a222c92f577cd3b55cba41",
127
+ "bytes": 21373
133
128
  },
134
129
  {
135
130
  "path": "yaaw-core/system/modules/changeability.md",
@@ -168,13 +163,13 @@
168
163
  },
169
164
  {
170
165
  "path": "yaaw-core/system/roles/planner.md",
171
- "sha256": "12208df84f8c57d3a40d6fc5ae6578da5299a2d8f611ceebd5ebd08e7828095b",
172
- "bytes": 7267
166
+ "sha256": "e12f34801181410064793530446578084c4e4fb4da944e60444875f3393f8798",
167
+ "bytes": 8646
173
168
  },
174
169
  {
175
170
  "path": "yaaw-core/system/roles/prd.md",
176
- "sha256": "9792407afb6ce55275b0acedf9727d7d79fbb4cfbd72aa5641078718c32e125a",
177
- "bytes": 3874
171
+ "sha256": "437c64a23575d353a86f8c81761394db99e855905c4f47beb6aad1dfa2f0efd6",
172
+ "bytes": 3372
178
173
  },
179
174
  {
180
175
  "path": "yaaw-core/system/roles/reviewer.md",
@@ -288,8 +283,8 @@
288
283
  },
289
284
  {
290
285
  "path": "yaaw-core/system/SYSTEM.md",
291
- "sha256": "47d318e533ded5347fadaf63089f631b167cf7a28fc64377aa7d21f34c3f3a5e",
292
- "bytes": 10940
286
+ "sha256": "b793fbfae235c63861d7b131ff2b379c8dae3abc8fad85be3d089f1261d61a73",
287
+ "bytes": 9772
293
288
  },
294
289
  {
295
290
  "path": "yaaw-core/system/templates/dispatch-failures.json",
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "schema": "yaaw.payload/v1",
3
- "version": "0.3.13"
3
+ "version": "0.3.14-dev.39042534464.1"
4
4
  }
@@ -82,10 +82,6 @@ Workers never privately command or spawn peer YAAW authorities. After every work
82
82
 
83
83
  Before semantic routing, verify the installer manifest and package-managed system bytes. `MODIFIED`, `MISSING`, `LOCAL_OVERRIDE`, unknown integrity, or contract inconsistency fails closed. Semantic workers never edit `.yaaw-core/system/**` to unblock themselves.
84
84
 
85
- Dispatch completeness is a framework invariant. A worker dispatch target is the union of non-Orchestrator public-skill requests, deterministic router targets (the values used by router branches), and declared worker handoffs. Other role operations are internal suboperations; root Orchestrator operations do not require worker handoffs. Every target must resolve a workflow, owning role, valid handoff, role-bounded reads/writes, active artifact, authorized existing modules, and the typed worker result protocol. Public skill metadata and completion vocabulary must agree with the kernel. `engine/contracts.mjs` is the canonical validator used by core, distribution, payload generation/verification, and runtime. Missing contracts stop as framework defects, never consumer semantic blockers.
86
-
87
- Product revision changes the downstream product basis. Orchestrator derives stale ticket authority and performs only legal status reconciliation; Planner owns changed downstream contracts. Engineering readiness based on an older product revision cannot authorize a specification. Clarity-only product refinement preserves semantic revision and downstream source authority.
88
-
89
85
  ## Repository identity
90
86
 
91
87
  Repository identity is produced only by `.yaaw-core/system/engine/repository-identity.mjs`; roles do not reimplement it. Runtime caches, ticket lifecycle files, immutable implementation evidence, and immutable reviews are excluded from the application worktree digest because their normal lifecycle writes would otherwise invalidate their own basis. Ticket semantic freshness is instead bound explicitly by ticket revision and ticket content digest in handoffs/evidence/reviews.
@@ -1,4 +1,3 @@
1
- import { ROUTES, dispatchableWorkflows } from "./contracts.mjs";
2
1
  const ACTIVE = new Set(["READY","IN_PROGRESS","REVIEW_REQUIRED","REPAIR_REQUIRED","PASS"]);
3
2
  const TERMINAL = new Set(["PASS","CANCELLED"]);
4
3
  const REVIEW_RESULT = { PASS:"PASS", REPAIR:"REPAIR_REQUIRED", REPLAN:"REPLAN_REQUIRED", BLOCKED:"BLOCKED" };
@@ -29,7 +28,6 @@ export function deriveState(a){
29
28
  const p=a.product?.meta??{},e=a.engineering?.meta??{};
30
29
  const product={artifact:a.product?.path??null,status:String(p.status??"missing"),revision:Number(p.revision??0)};
31
30
  const planning={artifact:a.engineering?.path??null,status:String(e.status??"missing"),revision:Number(e.revision??0),current_frontier:e.current_frontier??null,readiness:e.readiness??"pending",scope_status:e.scope_status??"UNKNOWN",active_spec:null};
32
- if(e.product_revision!==undefined&&Number(e.product_revision)!==product.revision)planning.readiness="pending";
33
31
  const accepted=Object.entries(a.specs??{}).filter(([,x])=>x.meta?.status==="ACCEPTED"&&Number(x.meta.product_revision??0)===product.revision&&Number(x.meta.engineering_revision??0)===planning.revision).sort(([x],[y])=>byId(x,y));
34
32
  if(accepted.length===1)planning.active_spec=accepted[0][1].path;
35
33
  const tickets=Object.fromEntries(Object.entries(a.tickets??{}).map(([id,x])=>[id,String(x.meta?.status??"DRAFT")]));
@@ -88,7 +86,7 @@ export function intentComplete(intent,state,a){
88
86
  if(!intent)return false;const o=intent.desired_outcome,target=requestedTicket(intent,state),base=intent.created_from??{};
89
87
  if(o==="CONTINUE")return false;
90
88
  if(o==="CONTINUE_PRODUCT")return state.product.status==="ready";
91
- if(o==="REVISE_PRODUCT")return state.product.status==="ready"&&Number(state.product.revision)>Number(base.product_revision??0);
89
+ if(o==="REVISE_PRODUCT")return Number(state.product.revision)>Number(base.product_revision??0);
92
90
  if(o==="REFINE_PRODUCT")return Boolean(a.product?.digest&&base.product_digest&&a.product.digest!==base.product_digest);
93
91
  if(o==="CONTINUE_PLANNING")return planningReady(state);
94
92
  if(o==="PLANNING_REVIEW")return state.planning.readiness!==base.planning_readiness||a.engineering?.digest!==base.engineering_digest;
@@ -100,24 +98,23 @@ export function intentComplete(intent,state,a){
100
98
  }
101
99
 
102
100
  function baseRoute(state,a){
103
- if(state.product.status!=="ready")return {workflow:ROUTES.product,ticket:null};
104
- const replan=Object.keys(state.tickets).sort(byId).find(id=>state.tickets[id]==="REPLAN_REQUIRED");if(replan)return{workflow:ROUTES.replan,ticket:replan};
105
- if(state.planning.status!=="ready"||state.planning.readiness!=="PASS")return{workflow:ROUTES.planning,ticket:null};
106
- if(!currentSpecId(state))return{workflow:ROUTES.spec,ticket:null};
101
+ if(state.product.status!=="ready")return {workflow:"prd.route",ticket:null};
102
+ const replan=Object.keys(state.tickets).sort(byId).find(id=>state.tickets[id]==="REPLAN_REQUIRED");if(replan)return{workflow:"planning.replan",ticket:replan};
103
+ if(state.planning.status!=="ready"||state.planning.readiness!=="PASS")return{workflow:"planning.route",ticket:null};
104
+ if(!currentSpecId(state))return{workflow:"planning.create-spec",ticket:null};
107
105
  const ids=currentTicketIds(state,a);
108
- const repair=ids.find(id=>state.tickets[id]==="REPAIR_REQUIRED");if(repair)return{workflow:ROUTES.repair,ticket:repair};
109
- const review=ids.find(id=>state.tickets[id]==="REVIEW_REQUIRED");if(review)return{workflow:ROUTES.review,ticket:review};
110
- const progress=ids.find(id=>state.tickets[id]==="IN_PROGRESS");if(progress)return{workflow:ROUTES.verify,ticket:progress};
111
- const ready=ids.find(id=>state.tickets[id]==="READY"&&dependenciesPass(id,state,a));if(ready)return{workflow:ROUTES.implement,ticket:ready};
112
- const draft=ids.find(id=>state.tickets[id]==="DRAFT"&&dependenciesPass(id,state,a));if(draft)return{workflow:ROUTES.tickets,ticket:draft};
113
- if(ids.length===0)return{workflow:ROUTES.tickets,ticket:null};
114
- if(ids.every(id=>TERMINAL.has(state.tickets[id]))){if(state.planning.scope_status==="COMPLETE")return{terminal:"COMPLETE"};return{workflow:ROUTES.planning,ticket:null};}
106
+ const repair=ids.find(id=>state.tickets[id]==="REPAIR_REQUIRED");if(repair)return{workflow:"implementation.repair-ticket",ticket:repair};
107
+ const review=ids.find(id=>state.tickets[id]==="REVIEW_REQUIRED");if(review)return{workflow:"review.review-ticket",ticket:review};
108
+ const progress=ids.find(id=>state.tickets[id]==="IN_PROGRESS");if(progress)return{workflow:"implementation.verify-ticket",ticket:progress};
109
+ const ready=ids.find(id=>state.tickets[id]==="READY"&&dependenciesPass(id,state,a));if(ready)return{workflow:"implementation.implement-ticket",ticket:ready};
110
+ const draft=ids.find(id=>state.tickets[id]==="DRAFT"&&dependenciesPass(id,state,a));if(draft)return{workflow:"planning.create-tickets",ticket:draft};
111
+ if(ids.length===0)return{workflow:"planning.create-tickets",ticket:null};
112
+ if(ids.every(id=>TERMINAL.has(state.tickets[id]))){if(state.planning.scope_status==="COMPLETE")return{terminal:"COMPLETE"};return{workflow:"planning.route",ticket:null};}
115
113
  const blocked=ids.find(id=>state.tickets[id]==="BLOCKED");if(blocked)return{blocked:`ticket ${blocked} is BLOCKED`,ticket:blocked};
116
114
  return{blocked:"no legal next workflow from current durable artifacts",ticket:null};
117
115
  }
118
116
 
119
- function legalRequested(kernel,workflow,ticket,state,a){
120
- if(!dispatchableWorkflows(kernel).has(workflow))return false;
117
+ function legalRequested(workflow,ticket,state,a){
121
118
  if(workflow==="prd.route"||workflow==="prd.revise"||workflow==="prd.refine")return true;
122
119
  if(workflow==="planning.route"||workflow==="planning.readiness-review")return state.product.status==="ready";
123
120
  if(workflow==="planning.create-spec")return planningReady(state)&&!currentSpecId(state);
@@ -131,14 +128,13 @@ function legalRequested(kernel,workflow,ticket,state,a){
131
128
  export function planNext(kernel,state,a,repo,intent){
132
129
  const reconciliation=selectReconciliation(kernel,state,a,repo);if(reconciliation)return{type:"RECONCILE_REQUIRED",reconciliation};
133
130
  if(intentComplete(intent,state,a))return{type:"INTENT_COMPLETE",completion_kind:intent.completion_kind};
134
- if(state.phase==="complete"&&!["prd.revise","prd.refine"].includes(intent?.requested_workflow))return{type:"TERMINAL",terminal:"COMPLETE"};
135
- if(intent?.desired_outcome==="REVISE_PRODUCT"&&Number(state.product.revision)>Number(intent.created_from?.product_revision??0)&&state.product.status!=="ready")return{type:"ROUTE",workflow:ROUTES.product,ticket:null};
131
+ if(state.phase==="complete")return{type:"TERMINAL",terminal:"COMPLETE"};
136
132
  if(intent&&intent.requested_workflow&&intent.requested_workflow!=="orchestration.route"){
137
133
  let ticket=requestedTicket(intent,state);
138
134
  if(!ticket&&intent.desired_outcome==="IMPLEMENT")ticket=Object.keys(state.tickets).sort(byId).find(id=>state.tickets[id]==="READY"&&dependenciesPass(id,state,a))??null;
139
135
  if(!ticket&&intent.desired_outcome==="REPAIR")ticket=Object.keys(state.tickets).sort(byId).find(id=>state.tickets[id]==="REPAIR_REQUIRED")??null;
140
136
  if(!ticket&&intent.desired_outcome==="REVIEW")ticket=Object.keys(state.tickets).sort(byId).find(id=>state.tickets[id]==="REVIEW_REQUIRED")??null;
141
- if(legalRequested(kernel,intent.requested_workflow,ticket,state,a))return{type:"ROUTE",workflow:intent.requested_workflow,ticket};
137
+ if(legalRequested(intent.requested_workflow,ticket,state,a))return{type:"ROUTE",workflow:intent.requested_workflow,ticket};
142
138
  return{type:"BLOCKED",reason:`PRECONDITION_UNSATISFIED:${intent.requested_workflow}`,ticket};
143
139
  }
144
140
  return{type:"ROUTE",...baseRoute(state,a)};
@@ -3,7 +3,6 @@ import { createHash } from "node:crypto";
3
3
  import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
4
  import { join, relative, resolve } from "node:path";
5
5
  import { spawnSync } from "node:child_process";
6
- import { validateContracts, validateDispatchFiles } from "./contracts.mjs";
7
6
  import { parseFrontmatter } from "./frontmatter.mjs";
8
7
  import { deriveState, planNext, chooseIntentTarget } from "./routing.mjs";
9
8
 
@@ -36,7 +35,7 @@ function refsFor(reads,state,a,ticket){const refs=[];const push=x=>{if(x&&!refs.
36
35
  function revisions(state,a,ticket){const spec=selectedSpec(state,a,ticket);return{product:Number(state.product.revision??0),engineering:Number(state.planning.revision??0),spec:Number(spec?.meta?.revision??0),ticket:Number(ticket?a.tickets?.[ticket]?.meta?.revision??0:0)}}
37
36
  function basis(state,a,ticket){const spec=selectedSpec(state,a,ticket);return{product_digest:a.product?.digest??null,engineering_digest:a.engineering?.digest??null,spec_digest:spec?.digest??null,ticket_digest:ticket?a.tickets?.[ticket]?.digest??null:null,ticket_status:ticket?state.tickets?.[ticket]??null:null}}
38
37
  function modulesFor(kernel,role,policy,a,ticket){const names=[...(policy.required_modules??[])];if(policy.ticket_modules&&ticket)for(const x of a.tickets?.[ticket]?.meta?.expertise??[])names.push(x);return[...new Set(names)].filter(name=>kernel.modules[name]?.usable_by?.includes(role)).map(name=>kernel.modules[name].path)}
39
- function handoff(kernel,framework,state,a,repo,workflow,ticket,intent){const wf=kernel.workflows[workflow],policy=kernel.handoffs[workflow],role=kernel.roles[wf?.role];if(!wf||!policy||!role)throw new Error(`no handoff contract for ${workflow}`);for(const kind of ["reads","writes"])for(const artifact of policy[kind]??[])if(!role[kind].includes(artifact))throw new Error(`workflow ${workflow} ${kind} unauthorized artifact ${artifact}`);return{schema:"yaaw.handoff/v4",framework:frameworkBasis(framework),role:wf.role,workflow,operation:wf.operation,instruction:{document:role.document,operation:wf.operation},desired_intent:intent?.desired_outcome??null,active_artifact:policy.active_artifact??null,ticket:ticket??null,references:refsFor(policy.reads,state,a,ticket),reads:policy.reads,writes:policy.writes,forbidden_writes:Object.keys(kernel.artifacts).filter(x=>!role.writes.includes(x)),revisions:revisions(state,a,ticket),basis:basis(state,a,ticket),expertise:modulesFor(kernel,wf.role,policy,a,ticket),host_expertise:[],expected_output:policy.expected_output,result_vocabulary:policy.result_vocabulary,repository_requirement:wf.repository_requirement,repository:repo}}
38
+ function handoff(kernel,framework,state,a,repo,workflow,ticket,intent){const wf=kernel.workflows[workflow],policy=kernel.handoffs[workflow],role=kernel.roles[wf?.role];if(!wf||!policy||!role)throw new Error(`no handoff contract for ${workflow}`);for(const w of policy.writes??[])if(!role.writes.includes(w))throw new Error(`workflow ${workflow} writes unauthorized artifact ${w}`);return{schema:"yaaw.handoff/v4",framework:frameworkBasis(framework),role:wf.role,workflow,operation:wf.operation,instruction:{document:role.document,operation:wf.operation},desired_intent:intent?.desired_outcome??null,active_artifact:policy.active_artifact??null,ticket:ticket??null,references:refsFor(policy.reads,state,a,ticket),reads:policy.reads,writes:policy.writes,forbidden_writes:Object.keys(kernel.artifacts).filter(x=>!role.writes.includes(x)),revisions:revisions(state,a,ticket),basis:basis(state,a,ticket),expertise:modulesFor(kernel,wf.role,policy,a,ticket),host_expertise:[],expected_output:policy.expected_output,result_vocabulary:policy.result_vocabulary,repository_requirement:wf.repository_requirement,repository:repo}}
40
39
  function repoAllowed(requirement,repo){if(requirement==="NONE")return true;if(requirement==="INSPECT")return repo?.status==="READY"||repo?.status==="UNVERSIONED";return repo?.status==="READY"}
41
40
  function patchStatus(text,status){const lines=String(text).split(/\r?\n/);if(lines[0]?.trim()!=="---")throw new Error("ticket missing frontmatter");const end=lines.slice(1).findIndex(x=>x.trim()==="---");if(end<0)throw new Error("ticket missing closing frontmatter");const idx=lines.slice(1,end+1).findIndex(x=>/^status:\s*/.test(x));if(idx<0)throw new Error("ticket missing status");lines[idx+1]=`status: ${status}`;return lines.join("\n")}
42
41
  async function observed(root,framework,repo,state,a,reconciliation){const value={schema:"yaaw.observed-state/v4",framework:{...frameworkBasis(framework),modified:framework.modified??[],missing:framework.missing??[],local_overrides:framework.local_overrides??[],repair_required:Boolean(framework.repair_required)},repository:repo,derived:state,reconstructable:true,artifacts:{product:a.product,engineering:a.engineering,specs:a.specs,tickets:a.tickets,reviews:a.reviews,evidence:a.evidence,research:a.research,rules:a.rules},inconsistencies:[],candidate_actions:[],reconciliation};await mkdir(join(root,".yaaw-core","runtime"),{recursive:true});await writeFile(join(root,".yaaw-core","runtime","observed-state.json"),JSON.stringify(value,null,2)+"\n");return value}
@@ -44,23 +43,15 @@ async function observed(root,framework,repo,state,a,reconciliation){const value=
44
43
  async function main(){
45
44
  const opt=argsOf(process.argv.slice(2)),root=resolve(opt.workspace),system=join(root,".yaaw-core","system"),runtime=join(root,".yaaw-core","runtime"),kernel=JSON.parse(await readFile(join(system,"kernel.yaml"),"utf8"));
46
45
  const integrity=execJson(join(system,"engine","integrity.mjs"),root);if(integrity.status!=="HEALTHY"){console.log(JSON.stringify({type:"FRAMEWORK_STOP",reason:`FRAMEWORK_${integrity.status}`,framework:integrity}));return}
47
- const contractErrors=validateContracts(kernel);
48
- if(contractErrors.length){
49
- const intent=await json(join(runtime,"intent.json")),sourceSkill=opt.invoke??intent?.source_skill??null,requested=kernel.skills[sourceSkill]?.requested_workflow??intent?.requested_workflow??null;
50
- const subject=contractErrors[0].split(": ")[0],workflow=kernel.workflows[subject]?subject:requested??subject;
51
- throw Object.assign(new Error(contractErrors.join("; ")),{diagnostic:{classification:"FRAMEWORK_DEFECT_OR_DAMAGED_INSTALLATION",workflow,requested_workflow:requested,source_skill:sourceSkill,routing_source:sourceSkill?"PUBLIC_SKILL_INTENT":"DETERMINISTIC_ROUTER",owning_role:kernel.workflows[workflow]?.role??null,framework_version:integrity.package_version,contract_type:"DISPATCH_CONTRACT",recommendation:"Update or repair YAAW-SE through the supported installer; preserve project memory."}});
52
- }
53
46
  const repo=execJson(join(system,"engine","repository-identity.mjs"),root),a=await inspectArtifacts(root),state=deriveState(a);
54
47
  let intent=await json(join(runtime,"intent.json"));if(opt.invoke)intent=await seedIntent(root,kernel,opt.invoke,state,a);
55
48
  const planned=planNext(kernel,state,a,repo,intent),reconciliation=planned.type==="RECONCILE_REQUIRED"?planned.reconciliation:null;await observed(root,integrity,repo,state,a,reconciliation);
56
49
  if(opt.inspect){console.log(JSON.stringify({type:"INSPECTED",state,reconciliation}));return}
57
50
  if(opt.reconcile){if(!reconciliation){console.log(JSON.stringify({type:"NO_RECONCILIATION"}));return}if(reconciliation.kind==="BLOCKED"){console.log(JSON.stringify({type:"BLOCKED",reason:reconciliation.reason}));return}const ticket=a.tickets?.[reconciliation.subject];if(!ticket)throw new Error(`reconciliation ticket missing: ${reconciliation.subject}`);const ticketPath=join(root,ticket.path),raw=await readFile(ticketPath,"utf8");await writeFile(ticketPath,patchStatus(raw,reconciliation.to),"utf8");for(const file of ["observed-state.json","handoff.json"])try{await writeFile(join(runtime,file),"")}catch{}console.log(JSON.stringify({type:"RECONCILED",reconciliation}));return}
58
- if(opt.check){const old=await json(join(runtime,"handoff.json"));if(!old){console.log(JSON.stringify({type:"HANDOFF_STALE",reason:"missing handoff"}));return}const candidate=handoff(kernel,integrity,state,a,repo,old.workflow,old.ticket,intent);const fileErrors=await validateDispatchFiles(system,kernel,old.workflow,candidate.expertise);if(fileErrors.length)throw new Error(fileErrors.join("; "));console.log(JSON.stringify(same(old,candidate)?{type:"HANDOFF_FRESH"}:{type:"HANDOFF_STALE",reason:"durable basis changed"}));return}
51
+ if(opt.check){const old=await json(join(runtime,"handoff.json"));if(!old){console.log(JSON.stringify({type:"HANDOFF_STALE",reason:"missing handoff"}));return}const candidate=handoff(kernel,integrity,state,a,repo,old.workflow,old.ticket,intent);console.log(JSON.stringify(same(old,candidate)?{type:"HANDOFF_FRESH"}:{type:"HANDOFF_STALE",reason:"durable basis changed"}));return}
59
52
  if(planned.type!=="ROUTE"){if(planned.type!=="RECONCILE_REQUIRED")try{await writeFile(join(runtime,"handoff.json"),"")}catch{}console.log(JSON.stringify(planned));return}
60
53
  if(planned.terminal){console.log(JSON.stringify({type:"TERMINAL",terminal:planned.terminal}));return}if(planned.blocked){console.log(JSON.stringify({type:"BLOCKED",reason:planned.blocked,ticket:planned.ticket??null}));return}
61
54
  const wf=kernel.workflows[planned.workflow];if(!wf)throw new Error(`unknown workflow ${planned.workflow}`);if(wf.role==="orchestrator"){console.log(JSON.stringify({type:"ROOT_ACTION",workflow:planned.workflow,ticket:planned.ticket??null}));return}if(!repoAllowed(wf.repository_requirement,repo)){console.log(JSON.stringify({type:"BLOCKED",reason:"PRECONDITION_UNSATISFIED:REPOSITORY_IDENTITY_UNAVAILABLE",workflow:planned.workflow}));return}
62
- const h=handoff(kernel,integrity,state,a,repo,planned.workflow,planned.ticket??null,intent);
63
- const fileErrors=await validateDispatchFiles(system,kernel,planned.workflow,h.expertise);if(fileErrors.length)throw Object.assign(new Error(fileErrors.join("; ")),{diagnostic:{classification:"FRAMEWORK_DEFECT_OR_DAMAGED_INSTALLATION",workflow:planned.workflow,source_skill:intent?.source_skill??null,owning_role:h.role,framework_version:integrity.package_version,contract_type:"INSTRUCTION_OR_MODULE_FILE",recommendation:"Update or repair YAAW-SE through the supported installer; preserve project memory."}});
64
- await mkdir(runtime,{recursive:true});await writeFile(join(runtime,"handoff.json"),JSON.stringify(h,null,2)+"\n");console.log(JSON.stringify({type:"DISPATCH_READY",workflow:planned.workflow,role:h.role,operation:h.operation,ticket:h.ticket,handoff:rel(root,join(runtime,"handoff.json"))}));
55
+ const h=handoff(kernel,integrity,state,a,repo,planned.workflow,planned.ticket??null,intent);await mkdir(runtime,{recursive:true});await writeFile(join(runtime,"handoff.json"),JSON.stringify(h,null,2)+"\n");console.log(JSON.stringify({type:"DISPATCH_READY",workflow:planned.workflow,role:h.role,operation:h.operation,ticket:h.ticket,handoff:rel(root,join(runtime,"handoff.json"))}));
65
56
  }
66
- main().catch(error=>{process.stdout.write(JSON.stringify({type:"FRAMEWORK_STOP",reason:"FRAMEWORK_CONTRACT_INCONSISTENCY",error:String(error?.message??error),...(error?.diagnostic?{diagnostic:error.diagnostic}:{})})+"\n");process.exitCode=2});
57
+ main().catch(error=>{process.stdout.write(JSON.stringify({type:"FRAMEWORK_STOP",reason:"FRAMEWORK_CONTRACT_INCONSISTENCY",error:String(error?.stack??error)})+"\n");process.exitCode=2});
@@ -76,9 +76,6 @@
76
76
  "orchestration.recover-interruption": {"role":"orchestrator","operation":"recover-interruption","repository_requirement":"IDENTITY","external_research":"forbidden","execution_context":"ROOT_ONLY"}
77
77
  },
78
78
  "handoffs": {
79
- "prd.revise": {"active_artifact":"product","reads":["product"],"writes":["product"],"required_modules":[],"ticket_modules":false,"expected_output":"Human-approved revised product meaning, incremented semantic revision and provenance; READY or unresolved product questions. Downstream staleness is derived by Orchestrator.","result_vocabulary":["READY","HUMAN_INPUT_REQUIRED","BLOCKED"]},
80
- "prd.refine": {"active_artifact":"product","reads":["product"],"writes":["product"],"required_modules":[],"ticket_modules":false,"expected_output":"Clarified product text preserving accepted meaning and semantic revision, or a typed product question/stop.","result_vocabulary":["READY","HUMAN_INPUT_REQUIRED","BLOCKED"]},
81
- "planning.readiness-review": {"active_artifact":"engineering","reads":["product","engineering","engineering_research","spec","ticket","project_rule","repository"],"writes":["engineering"],"required_modules":[],"ticket_modules":false,"expected_output":"Current-frontier readiness result, source revisions, reason and evidence persisted only in engineering.md.","result_vocabulary":["PASS","MISSING_DECISIONS","PRODUCT_GAP","REPLAN","BLOCKED"]},
82
79
  "prd.route": {"active_artifact":"product","reads":["product"],"writes":["product"],"required_modules":[],"ticket_modules":false,"expected_output":"Current product intent or a typed stop.","result_vocabulary":["READY","HUMAN_INPUT_REQUIRED","BLOCKED"]},
83
80
  "planning.route": {"active_artifact":"engineering","reads":["product","engineering","engineering_research","spec","ticket","project_rule","repository"],"writes":["engineering","engineering_research","spec","ticket","project_rule"],"required_modules":[],"ticket_modules":false,"expected_output":"Current engineering frontier readiness or one typed planning result.","result_vocabulary":["READY","HUMAN_INPUT_REQUIRED","REPLAN","BLOCKED"]},
84
81
  "planning.create-spec": {"active_artifact":"engineering","reads":["product","engineering","engineering_research","project_rule","repository"],"writes":["spec"],"required_modules":[],"ticket_modules":false,"expected_output":"One accepted specification or a typed prerequisite/stop.","result_vocabulary":["SUCCESS","PRECONDITION_UNSATISFIED","BLOCKED"]},
@@ -21,10 +21,14 @@ Partition every material issue into known reusable decisions, one bounded curren
21
21
  Resolve exactly one current PENDING research artifact whose product/engineering/frontier basis is still current and admitted. Prefer primary sources; record version/date for sensitive facts and conflicts rather than choosing silently. Mark RESOLVED or BLOCKED. Research establishes facts only; return to decision-frontier before promoting anything into an `ENG-*` decision.
22
22
 
23
23
  ## Operation: question-round (`planning.question-round`)
24
- Load only the current frontier and evidence needed for it. Challenge material engineering assumptions, contradictions, failure/data/security/migration/testing/operability gaps, and premature architecture. Eliminate repository-answerable, routine reversible, future-dependent, and prerequisite-blocked questions. Product gaps return to PRD. Do not reopen settled `ENG-*` decisions without new evidence/contradiction/changed upstream intent. Ask at most 10 material questions and stop for human input.
24
+ Load only the current frontier and evidence needed for it. Challenge material engineering assumptions, contradictions, failure/data/security/migration/testing/operability gaps, and premature architecture. Eliminate repository-answerable, routine reversible, future-dependent, and prerequisite-blocked questions. Product gaps return to PRD. Do not reopen settled `ENG-*` decisions without new evidence/contradiction/changed upstream intent.
25
+
26
+ When human input is genuinely required, ask one batch of **1 to 10** material questions; never pad a batch to reach 10. For every user-owned decision or clarification that has two or more credible alternatives, present explicit labeled choices (`A.`, `B.`, `C.`, ...), then a single `Recommended: <label> — <concise reason>` line. The recommendation must state the engineering tradeoff that makes that option preferable within accepted product intent; it is advice, not new product authority. Do not manufacture choices for factual values, genuinely open-ended clarifications, or cases where labeled alternatives would hide the real decision; ask those directly instead. Free-form answers, modified options, combinations, and label-only answers are all valid. If no valid human-owned question remains, do not invent one merely to satisfy the range; return to the frontier/readiness flow.
27
+
28
+ Stop after the batch for human input. Do not start another question round until accepted answers from the current batch have been processed by `planning.record-decisions` and the decision frontier has been recomputed.
25
29
 
26
30
  ## Operation: record-decisions (`planning.record-decisions`)
27
- Validate accepted answers against current product authority and repository evidence. Product meaning gaps return to PRD. Conflicts with accepted `ENG-*` decisions require explicit supersession and invalidation. Otherwise create/update durable decisions with Status, Decision, Reason, material rejected alternatives, implications, and provenance. Increment engineering revision for material contract changes, persist conclusions rather than transcripts, and mark frontier/readiness stale.
31
+ Validate accepted answers against current product authority and repository evidence. Product meaning gaps return to PRD. Conflicts with accepted `ENG-*` decisions require explicit supersession and invalidation. Otherwise create/update durable decisions with Status, Decision, Reason, material rejected alternatives, implications, and provenance. Treat labeled selections as shorthand for the corresponding current-batch alternative, while preserving any free-form modification or combination the human supplied; never persist a bare option label as engineering meaning. If the intended meaning of an answer cannot be determined from the current question batch, do not guess. Increment engineering revision for material contract changes, persist conclusions rather than transcripts, and mark frontier/readiness stale.
28
32
 
29
33
  ## Operation: readiness-review (`planning.readiness-review`)
30
34
  Freshly decide: could a fresh Implementer execute the next bounded frontier without inventing material product or architecture decisions? Check boundedness, assumptions, blocking research, repository contradiction, ambiguous terminology, failure/state transitions, hidden dependencies, product gaps, and repository identity availability for executable ticket admission. Return `PASS`, `MISSING_DECISIONS`, `PRODUCT_GAP`, `REPLAN`, or `BLOCKED`; persist result, frontier/source revisions, reason, and evidence in `engineering.md` only.
@@ -15,13 +15,13 @@ Initialize `product.md` from the canonical template only when missing; never ove
15
15
  Challenge only material contradictions, ambiguous terminology, unsupported assumptions, premature abstractions, important flows/failures/edge cases, and scope/non-goal conflicts. Exclude engineering-only questions and questions whose prerequisites remain unresolved. Ask at most 10 high-leverage questions; options/recommendation are optional and free-form answers are first-class. Stop for human input.
16
16
 
17
17
  ## Operation: record-decisions (`prd.record-decisions`)
18
- Interpret only what the answer supports. Persist conclusions in the relevant product sections, not the question/challenge transcript. Remove settled questions, preserve corrections/non-goals, and increment product revision only when accepted product meaning materially changes. Recompute contradictions/questions and expose the changed product basis for downstream invalidation. Orchestrator derives staleness; Planner alone revises engineering/spec/ticket contracts.
18
+ Interpret only what the answer supports. Persist conclusions in the relevant product sections, not the question/challenge transcript. Remove settled questions, preserve corrections/non-goals, and increment product revision only when accepted product meaning materially changes. Recompute contradictions/questions and invalidate downstream planning/spec/ticket authority whose product basis became stale.
19
19
 
20
20
  ## Operation: revise (`prd.revise`)
21
- Identify the exact old requirement and requested new meaning. Stress-test impact on behavior, scope, constraints, non-goals, and newly exposed product questions without drifting into engineering design. Update only `product.md`, increment revision for a material accepted meaning change, and record provenance. The changed product revision invalidates downstream authority through Orchestrator observation/reconciliation; do not mutate engineering, specifications, or tickets. Leave product draft if material questions remain; otherwise run readiness. Return `READY`, `HUMAN_INPUT_REQUIRED`, or `BLOCKED` for the dispatched invocation.
21
+ Identify the exact old requirement and requested new meaning. Stress-test impact on behavior, scope, constraints, non-goals, and newly exposed product questions without drifting into engineering design. Update `product.md`, increment revision, record provenance, and invalidate downstream dependent contracts. Leave product draft if material questions remain; otherwise run readiness.
22
22
 
23
23
  ## Operation: refine (`prd.refine`)
24
- Improve clarity, organization, deduplication, and completeness without changing accepted semantic meaning or product revision. If the edit changes behavior, scope, constraints, or non-goals—or exposes a genuine semantic contradiction—stop with `HUMAN_INPUT_REQUIRED` for explicit product revision authority instead of silently changing meaning. Return `READY` only when clarity work is complete and product readiness holds; otherwise return `HUMAN_INPUT_REQUIRED` or `BLOCKED`.
24
+ Improve clarity, organization, deduplication, and completeness without changing accepted semantic meaning or product revision. If the edit changes behavior, scope, constraints, or non-goals—or exposes a genuine semantic contradiction—switch to revision instead of silently changing meaning.
25
25
 
26
26
  ## Operation: readiness (`prd.readiness`)
27
27
  Ask whether a fresh Planner can understand goal, users, expected behavior, scope, constraints, non-goals, and remaining product unknowns without the original chat, and whether any material current-frontier ambiguity would force invented product intent. Return `READY`, `NEEDS_QUESTIONS`, or `BLOCKED`. On READY, persist `status: ready` in `product.md`. Future fog is allowed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaaw-se",
3
- "version": "0.3.13",
3
+ "version": "0.3.14-dev.39042534464.1",
4
4
  "description": "Artifact-first autonomous software engineering workflow installer.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,10 +28,7 @@
28
28
  "prepack": "npm run test && npm run build",
29
29
  "smoke:tarball": "npm run build && node scripts/smoke_npm_tarball.mjs",
30
30
  "smoke:update": "npm run build && node scripts/smoke_npm_update.mjs",
31
- "validate:schemas": "node scripts/validate_schemas.mjs",
32
- "validate:contracts": "node scripts/validate_contracts.mjs .yaaw-core/system skills",
33
- "smoke:dispatch-update": "npm run build && node scripts/smoke_dispatch_update.mjs",
34
- "audit:workflows": "node scripts/audit_workflow_contracts.mjs .yaaw-core/system skills"
31
+ "validate:schemas": "node scripts/validate_schemas.mjs"
35
32
  },
36
33
  "dependencies": {
37
34
  "@clack/prompts": "1.8.1",
@@ -1,147 +0,0 @@
1
- import { readFile, lstat, realpath } from 'node:fs/promises';
2
- import { resolve, relative, isAbsolute, join } from 'node:path';
3
- // The router uses these values at each branch; there is no separate target inventory.
4
- export const ROUTES = Object.freeze({product:'prd.route',planning:'planning.route',spec:'planning.create-spec',tickets:'planning.create-tickets',replan:'planning.replan',repair:'implementation.repair-ticket',review:'review.review-ticket',verify:'implementation.verify-ticket',implement:'implementation.implement-ticket'});
5
- export const ROUTER_DISPATCH_TARGETS = Object.freeze([...new Set(Object.values(ROUTES))]);
6
- // Typed worker outcomes consumed by the host continuation/lifecycle protocol.
7
- const RESULTS = {
8
- 'prd.route':['READY','HUMAN_INPUT_REQUIRED','BLOCKED'],
9
- 'prd.revise':['READY','HUMAN_INPUT_REQUIRED','BLOCKED'],
10
- 'prd.refine':['READY','HUMAN_INPUT_REQUIRED','BLOCKED'],
11
- 'planning.route':['READY','HUMAN_INPUT_REQUIRED','REPLAN','BLOCKED'],
12
- 'planning.readiness-review':['PASS','MISSING_DECISIONS','PRODUCT_GAP','REPLAN','BLOCKED'],
13
- 'planning.create-spec':['SUCCESS','PRECONDITION_UNSATISFIED','BLOCKED'],
14
- 'planning.create-tickets':['SUCCESS','PRECONDITION_UNSATISFIED','BLOCKED'],
15
- 'planning.replan':['READY','HUMAN_INPUT_REQUIRED','BLOCKED'],
16
- 'implementation.implement-ticket':['VERIFICATION_PASS','VERIFICATION_FAIL','REPLAN_REQUIRED','PRECONDITION_UNSATISFIED','BLOCKED'],
17
- 'implementation.verify-ticket':['VERIFICATION_PASS','VERIFICATION_FAIL','IMPLEMENTATION_INCOMPLETE','REPLAN_REQUIRED','BLOCKED'],
18
- 'implementation.repair-ticket':['VERIFICATION_PASS','VERIFICATION_FAIL','REPLAN_REQUIRED','BLOCKED'],
19
- 'review.review-ticket':['PASS','REPAIR','REPLAN','BLOCKED']
20
- };
21
- // A shortcut's completion must describe the operation it actually requests.
22
- const INTENTS = {
23
- CONTINUE:['orchestration.route','CONTINUE_UNTIL_STOP'],
24
- CONTINUE_PRODUCT:['prd.route','PRODUCT_READY'],
25
- REVISE_PRODUCT:['prd.revise','PRODUCT_REVISION_ADVANCED'],
26
- REFINE_PRODUCT:['prd.refine','PRODUCT_REFINED'],
27
- CONTINUE_PLANNING:['planning.route','PLANNING_READY'],
28
- PLANNING_REVIEW:['planning.readiness-review','PLANNING_READINESS_DECIDED'],
29
- CREATE_SPEC:['planning.create-spec','CURRENT_SPEC_ACCEPTED'],
30
- CREATE_TICKETS:['planning.create-tickets','CURRENT_TICKETS_REGISTERED'],
31
- IMPLEMENT:['implementation.implement-ticket','TICKET_REVIEW_REQUIRED'],
32
- REPAIR:['implementation.repair-ticket','TICKET_REVIEW_REQUIRED'],
33
- REVIEW:['review.review-ticket','TICKET_REVIEW_APPLIED']
34
- };
35
- export function dispatchableWorkflows(kernel) {
36
- return new Set([...ROUTER_DISPATCH_TARGETS,...Object.values(kernel.skills??{}).filter(s=>s.role!=='orchestrator').map(s=>s.requested_workflow),...Object.keys(kernel.handoffs??{})]);
37
- }
38
- const managedPath=p=>typeof p==='string'&&p.startsWith('.yaaw-core/system/')&&!p.includes('\\')&&!p.split('/').includes('..')&&p.endsWith('.md');
39
- async function managedFile(system,path) {
40
- if(!managedPath(path))throw new Error(`invalid managed system reference ${path}`);
41
- const target=resolve(system,path.slice('.yaaw-core/system/'.length)),root=await realpath(system),actual=await realpath(target),rel=relative(root,actual);
42
- if(rel.startsWith('..')||isAbsolute(rel)||!(await lstat(target)).isFile())throw new Error(`invalid managed system file ${path}`);
43
- return target;
44
- }
45
- const operationHeader=(id,w)=>`## Operation: ${w.operation} (\`${id}\`)`;
46
- export async function validateDispatchFiles(system,k,workflow,expertise=[]) {
47
- const paths=[k.roles[k.workflows[workflow]?.role]?.document,...expertise],errors=[];
48
- for(const path of paths)try{await managedFile(system,path)}catch{errors.push(`${workflow}: missing/invalid dispatch file ${path}`)}
49
- const w=k.workflows[workflow];
50
- try{const text=await readFile(await managedFile(system,k.roles[w.role].document),"utf8");if(!text.includes(operationHeader(workflow,w)))errors.push(`${workflow}: missing selected role operation`)}catch{}
51
- return errors;
52
- }
53
- const stringList=x=>Array.isArray(x)&&x.every(v=>typeof v==='string'&&v.length>0)&&new Set(x).size===x.length;
54
- const sameSet=(a,b)=>stringList(a)&&stringList(b)&&a.length===b.length&&a.every(x=>b.includes(x));
55
- export function validateContracts(k) {
56
- const errors=[],roles=k.roles??{},workflows=k.workflows??{},artifacts=k.artifacts??{},modules=k.modules??{},handoffs=k.handoffs??{};
57
- const error=(id,message)=>errors.push(`${id}: ${message}`);
58
- for(const [id,a] of Object.entries(artifacts)) {
59
- if(!roles[a.owner]&&!['lifecycle','installer','none'].includes(a.owner))error(id,'unknown artifact owner');
60
- if(a.embedded_in&&!artifacts[a.embedded_in])error(id,'unknown embedded artifact');
61
- if(!a.path&&!a.path_pattern&&!a.embedded_in&&!a.virtual)error(id,'artifact has no locator');
62
- }
63
- for(const [id,r] of Object.entries(roles)){
64
- if(!managedPath(r.document))error(id,'invalid role document reference');
65
- for(const kind of ['reads','writes']) {
66
- if(!stringList(r[kind]))error(id,`invalid role ${kind}`);
67
- else for(const a of r[kind])if(!artifacts[a])error(id,`unknown ${kind} artifact ${a}`);
68
- }
69
- }
70
- for(const [id,w] of Object.entries(workflows)) {
71
- if(!roles[w.role])error(id,`unknown owning role ${w.role}`);
72
- if(typeof w.operation!=='string'||!w.operation)error(id,'missing operation');
73
- if(!['NONE','INSPECT','IDENTITY'].includes(w.repository_requirement))error(id,'invalid repository requirement');
74
- }
75
- for(const [id,s] of Object.entries(k.skills??{})) {
76
- const w=workflows[s.requested_workflow];
77
- if(!w)error(id,`unknown requested workflow ${s.requested_workflow}`);
78
- else if(w.role!==s.role)error(id,'skill role differs from workflow owner');
79
- if(!roles[s.role])error(id,'unknown skill role');
80
- const intent=INTENTS[s.desired_outcome];
81
- if(!intent||s.requested_workflow!==intent[0]||s.completion_kind!==intent[1])error(id,'shortcut intent/workflow/completion semantics disagree');
82
- }
83
- for(const id of dispatchableWorkflows(k)) {
84
- const w=workflows[id];
85
- if(!w)error(id,'dispatch target has no workflow');
86
- if(w?.role==='orchestrator')error(id,'root operation cannot be a worker dispatch target');
87
- if(!handoffs[id])error(id,'dispatch target has no handoff contract');
88
- }
89
- for(const [id,h] of Object.entries(handoffs)) {
90
- const w=workflows[id],r=roles[w?.role];
91
- if(!w||!r){error(id,'handoff has no workflow/owning role');continue}
92
- for(const kind of ['reads','writes']) {
93
- if(!stringList(h[kind])){error(id,`invalid handoff ${kind}`);continue}
94
- for(const a of h[kind]) {
95
- if(!artifacts[a])error(id,`unknown ${kind} artifact ${a}`);
96
- if(!r[kind]?.includes(a))error(id,`${kind} outside ${w.role} authority: ${a}`);
97
- }
98
- }
99
- if(!artifacts[h.active_artifact])error(id,`unknown active artifact ${h.active_artifact}`);
100
- else if(!h.reads?.includes(h.active_artifact)&&!h.writes?.includes(h.active_artifact))error(id,'active artifact outside handoff I/O');
101
- if(!stringList(h.required_modules))error(id,'invalid required modules');
102
- else for(const m of h.required_modules)if(!modules[m]?.usable_by?.includes(w.role))error(id,`missing/unauthorized module ${m}`);
103
- if(typeof h.ticket_modules!=='boolean')error(id,'invalid ticket_modules');
104
- if(typeof h.expected_output!=='string'||!h.expected_output.trim())error(id,'missing expected output');
105
- if(!sameSet(h.result_vocabulary,RESULTS[id]))error(id,'result vocabulary incompatible with worker protocol');
106
- }
107
- for(const [id,m] of Object.entries(modules)) {
108
- if(!managedPath(m.path))error(id,'invalid module path reference');
109
- if(!stringList(m.usable_by)||m.usable_by.some(r=>!roles[r]))error(id,'unknown module role');
110
- }
111
- for(const row of k.lifecycle?.legal??[]) {
112
- if(!workflows[row.workflow]||!roles[row.owner])error('lifecycle','unresolved workflow/owner');
113
- if(row.writer&&!roles[row.writer])error('lifecycle','unknown writer');
114
- }
115
- const routing=k.lifecycle?.routing??{};
116
- for(const [name,value] of Object.entries(routing)) {
117
- if(name.endsWith('_workflow')&&!ROUTER_DISPATCH_TARGETS.includes(value))error(name,'routing workflow is not a router target');
118
- }
119
- for(const row of Object.values(routing.invalidation_routes??{}))if(!ROUTER_DISPATCH_TARGETS.includes(row.workflow))error('invalidation_routes','unknown router target');
120
- return errors;
121
- }
122
- export async function validateSystemContracts(system,skills=null) {
123
- const k=JSON.parse(await readFile(join(system,'kernel.yaml'),'utf8')),errors=validateContracts(k);
124
- async function textAt(path,id) {
125
- try{return await readFile(await managedFile(system,path),'utf8')}catch{errors.push(`${id}: missing/invalid referenced file ${path}`);return ''}
126
- }
127
- for(const [id,r] of Object.entries(k.roles??{})) {
128
- const text=await textAt(r.document,id);
129
- for(const [wid,w] of Object.entries(k.workflows??{}))if(w.role===id&&!text.includes(operationHeader(wid,w)))errors.push(`${wid}: missing role operation`);
130
- }
131
- for(const [id,m] of Object.entries(k.modules??{}))await textAt(m.path,id);
132
- if(skills) {
133
- const {readdir}=await import('node:fs/promises');
134
- const dirs=(await readdir(skills,{withFileTypes:true})).filter(e=>e.isDirectory()).map(e=>e.name);
135
- if(!sameSet(dirs,Object.keys(k.skills??{})))errors.push('public skill directory/catalog mismatch');
136
- for(const [id,s] of Object.entries(k.skills??{})) {
137
- try {
138
- const text=await readFile(join(skills,id,'SKILL.md'),'utf8');
139
- for(const marker of [`name: ${id}`,`ROLE: \`${s.role}\``,`WORKFLOW: \`${s.requested_workflow}\``,`INTENT: \`${s.desired_outcome}\``, 'system/engine/runtime.mjs',`--invoke-skill ${id}`])if(!text.includes(marker))errors.push(`${id}: skill metadata/runtime invocation mismatch (${marker})`);
140
- }catch{errors.push(`${id}: missing public skill`)}
141
- }
142
- }
143
- // Completion and intent enums are already machine contracts; do not duplicate them.
144
- const schema=JSON.parse(await readFile(join(system,'schemas/intent.schema.json'),'utf8'));
145
- for(const [id,s] of Object.entries(k.skills??{}))for(const field of ['desired_outcome','completion_kind'])if(!schema.properties[field].enum.includes(s[field]))errors.push(`${id}: invalid ${field}`);
146
- return errors;
147
- }