@captain_z/zsk 1.8.1 → 1.8.2
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/dist/bin.js +180 -1
- package/dist/bin.js.map +1 -1
- package/dist/commands/check.js +75 -4
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/config.d.ts +11 -0
- package/dist/commands/config.js +132 -3
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/demo.js +2 -2
- package/dist/commands/demo.js.map +1 -1
- package/dist/commands/dispatch.d.ts +11 -0
- package/dist/commands/dispatch.js +69 -0
- package/dist/commands/dispatch.js.map +1 -0
- package/dist/commands/gate.d.ts +9 -0
- package/dist/commands/gate.js +48 -0
- package/dist/commands/gate.js.map +1 -0
- package/dist/commands/prep.d.ts +2 -4
- package/dist/commands/prep.js +1 -131
- package/dist/commands/prep.js.map +1 -1
- package/dist/commands/prepare.d.ts +17 -0
- package/dist/commands/prepare.js +259 -0
- package/dist/commands/prepare.js.map +1 -0
- package/dist/commands/project-init.d.ts +1 -0
- package/dist/commands/project-init.js +12 -10
- package/dist/commands/project-init.js.map +1 -1
- package/dist/commands/template.d.ts +2 -0
- package/dist/commands/template.js +34 -0
- package/dist/commands/template.js.map +1 -0
- package/dist/core/config.d.ts +85 -1
- package/dist/core/config.js +141 -7
- package/dist/core/config.js.map +1 -1
- package/dist/core/origin-detection.d.ts +10 -0
- package/dist/core/origin-detection.js +135 -0
- package/dist/core/origin-detection.js.map +1 -0
- package/dist/core/prepare-lifecycle.d.ts +54 -0
- package/dist/core/prepare-lifecycle.js +302 -0
- package/dist/core/prepare-lifecycle.js.map +1 -0
- package/dist/core/prepare-sync.d.ts +82 -0
- package/dist/core/prepare-sync.js +1499 -0
- package/dist/core/prepare-sync.js.map +1 -0
- package/dist/core/raw-manifest.d.ts +10 -0
- package/dist/core/raw-manifest.js +58 -4
- package/dist/core/raw-manifest.js.map +1 -1
- package/dist/core/source-draft.d.ts +14 -0
- package/dist/core/source-draft.js +251 -0
- package/dist/core/source-draft.js.map +1 -0
- package/dist/core/staffing-plan.d.ts +206 -0
- package/dist/core/staffing-plan.js +1115 -0
- package/dist/core/staffing-plan.js.map +1 -0
- package/dist/core/stage-quality.d.ts +56 -0
- package/dist/core/stage-quality.js +487 -0
- package/dist/core/stage-quality.js.map +1 -0
- package/dist/core/template-registry.d.ts +29 -0
- package/dist/core/template-registry.js +289 -0
- package/dist/core/template-registry.js.map +1 -0
- package/dist/core/workspace-layout.d.ts +3 -0
- package/dist/core/workspace-layout.js +14 -1
- package/dist/core/workspace-layout.js.map +1 -1
- package/package.json +2 -2
- package/schemas/zsk-config.schema.json +233 -196
- package/templates/module/frontend-module/design.md +71 -0
- package/templates/module/frontend-module/proposal.md +17 -0
- package/templates/module/frontend-module/spec.md +17 -0
- package/templates/module/frontend-module/tasks.md +36 -6
- package/templates/project-init/.zsk/config.yaml +8 -96
- package/templates/project-init/.zsk/raws/index.md +8 -0
- package/templates/project-init/.zsk/raws/prepare/backend/index.md +4 -0
- package/templates/project-init/.zsk/raws/prepare/design/index.md +3 -0
- package/templates/project-init/.zsk/raws/prepare/index.md +4 -0
- package/templates/project-init/.zsk/raws/prepare/product/index.md +4 -0
- package/templates/project-init/.zsk/raws/prepare/qa/index.md +4 -0
- package/templates/project-init/.zsk/raws/prepare/ux/index.md +3 -0
- package/templates/project-init/.zsk/roles.yaml +129 -0
- package/templates/project-init/.zsk/raws/backend/index.md +0 -3
- package/templates/project-init/.zsk/raws/jira/index.md +0 -3
- package/templates/project-init/.zsk/raws/manual/index.md +0 -3
- package/templates/project-init/.zsk/raws/product/index.md +0 -3
- package/templates/project-init/.zsk/raws/qa/index.md +0 -3
- package/templates/project-init/.zsk/raws/ue/index.md +0 -3
|
@@ -0,0 +1,1115 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { flattenProjectSources } from "./config.js";
|
|
5
|
+
import { inferSourceOrigin } from "./origin-detection.js";
|
|
6
|
+
import { gateThresholdForStage, writeGateAssessment } from "./stage-quality.js";
|
|
7
|
+
import { getWorkspacePath } from "./workspace-layout.js";
|
|
8
|
+
const DEFAULT_PACKET_TIMEOUT_POLICY = {
|
|
9
|
+
heartbeatIntervalMs: 60_000,
|
|
10
|
+
staleAfterMs: 600_000,
|
|
11
|
+
maxAttempts: 2,
|
|
12
|
+
onTimeout: "fallback-to-leader-sequential",
|
|
13
|
+
};
|
|
14
|
+
const BUILTIN_ROLE_POOL = {
|
|
15
|
+
roles: Object.fromEntries([
|
|
16
|
+
rolePoolEntry("lead-integrator", {
|
|
17
|
+
subagentType: "planner",
|
|
18
|
+
required: true,
|
|
19
|
+
activation: "required",
|
|
20
|
+
reason: "own stage scope, dispatch boundaries, integration, and final evidence",
|
|
21
|
+
writeScope: [
|
|
22
|
+
".zsk/config.yaml",
|
|
23
|
+
".zsk/roles.yaml",
|
|
24
|
+
".zsk/raws/manifest.json",
|
|
25
|
+
".zsk/raws/**/index.md",
|
|
26
|
+
".zsk/evidence/**",
|
|
27
|
+
".zsk/plans/**",
|
|
28
|
+
],
|
|
29
|
+
}),
|
|
30
|
+
rolePoolEntry("product-owner", {
|
|
31
|
+
subagentType: "analyst",
|
|
32
|
+
stages: ["prepare", "proposal", "spec", "review", "verify", "acceptance"],
|
|
33
|
+
lanes: ["product", "pm", "requirement", "requirements", "srs", "prd"],
|
|
34
|
+
activation: "when product or requirement evidence is configured or in scope",
|
|
35
|
+
reason: "interpret product scope, requirement semantics, priorities, and acceptance gaps",
|
|
36
|
+
}),
|
|
37
|
+
rolePoolEntry("business-analyst", {
|
|
38
|
+
subagentType: "analyst",
|
|
39
|
+
stages: ["proposal", "spec"],
|
|
40
|
+
lanes: ["business", "domain", "process", "rules"],
|
|
41
|
+
activation: "when domain process or business-rule evidence is configured or in scope",
|
|
42
|
+
reason: "map domain process, terminology, business rules, and edge cases",
|
|
43
|
+
}),
|
|
44
|
+
rolePoolEntry("architect", {
|
|
45
|
+
subagentType: "architect",
|
|
46
|
+
stages: ["proposal", "spec", "design", "review"],
|
|
47
|
+
activation: "when architecture, API, system-boundary, or dependency decisions are in scope",
|
|
48
|
+
reason: "review system boundaries, module relationships, contracts, and technical risks",
|
|
49
|
+
}),
|
|
50
|
+
rolePoolEntry("backend-engineer", {
|
|
51
|
+
subagentType: "executor",
|
|
52
|
+
stages: ["prepare", "design"],
|
|
53
|
+
lanes: ["backend", "api", "service", "server", "repository", "repo", "repos"],
|
|
54
|
+
activation: "when backend, API, service, repository, or data sources are configured",
|
|
55
|
+
reason: "inspect backend sources, API contracts, data models, and service-boundary gaps",
|
|
56
|
+
}),
|
|
57
|
+
rolePoolEntry("frontend-engineer", {
|
|
58
|
+
subagentType: "executor",
|
|
59
|
+
stages: ["design"],
|
|
60
|
+
lanes: ["frontend", "web", "client", "mobile"],
|
|
61
|
+
activation: "when frontend implementation, routing, state, or UI integration is in scope",
|
|
62
|
+
reason: "map UI implementation, routing, state, and component integration risk",
|
|
63
|
+
}),
|
|
64
|
+
rolePoolEntry("design-specialist", {
|
|
65
|
+
subagentType: "designer",
|
|
66
|
+
stages: ["prepare", "design"],
|
|
67
|
+
lanes: ["design", "ui", "visual"],
|
|
68
|
+
activation: "when design, UI, visual, prototype, or asset sources are configured",
|
|
69
|
+
reason: "inspect design assets, screens, visual states, and design-source gaps",
|
|
70
|
+
}),
|
|
71
|
+
rolePoolEntry("ux-specialist", {
|
|
72
|
+
subagentType: "designer",
|
|
73
|
+
stages: ["prepare"],
|
|
74
|
+
lanes: ["ux", "ue", "user-experience", "research"],
|
|
75
|
+
activation: "when UX, UE, user-flow, or research sources are configured",
|
|
76
|
+
reason: "inspect user flows, interaction constraints, and usability ambiguity",
|
|
77
|
+
}),
|
|
78
|
+
rolePoolEntry("qa-engineer", {
|
|
79
|
+
subagentType: "test-engineer",
|
|
80
|
+
stages: ["prepare", "spec", "task", "coding", "review", "verify"],
|
|
81
|
+
lanes: ["qa", "test", "testing", "quality"],
|
|
82
|
+
activation: "when QA, test, acceptance, or quality evidence is configured or in scope",
|
|
83
|
+
reason: "connect requirements to acceptance scenarios, test coverage, and verification evidence",
|
|
84
|
+
}),
|
|
85
|
+
rolePoolEntry("security-reviewer", {
|
|
86
|
+
subagentType: "security-reviewer",
|
|
87
|
+
stages: ["spec", "design", "review"],
|
|
88
|
+
lanes: ["security", "sec", "auth"],
|
|
89
|
+
activation: "when auth, permission, privacy, or data-safety boundaries are in scope",
|
|
90
|
+
reason: "review trust boundaries, credentials, privacy, and abuse cases",
|
|
91
|
+
}),
|
|
92
|
+
rolePoolEntry("operations-engineer", {
|
|
93
|
+
subagentType: "executor",
|
|
94
|
+
lanes: ["ops", "devops", "release", "deploy", "platform"],
|
|
95
|
+
activation: "when environment, CI/CD, deployment, or platform risks are configured or in scope",
|
|
96
|
+
reason: "review release, deployment, rollback, and operational risks",
|
|
97
|
+
}),
|
|
98
|
+
rolePoolEntry("auth-fetch-agent", {
|
|
99
|
+
subagentType: "researcher",
|
|
100
|
+
stages: ["prepare"],
|
|
101
|
+
remoteSources: true,
|
|
102
|
+
activation: "when remote or provider-managed sources are configured",
|
|
103
|
+
reason: "materialize remote/provider sources through explicit auth, session, and fetch contracts",
|
|
104
|
+
}),
|
|
105
|
+
rolePoolEntry("researcher", {
|
|
106
|
+
subagentType: "researcher",
|
|
107
|
+
stages: ["prepare"],
|
|
108
|
+
activation: "optional when current official or external evidence is required",
|
|
109
|
+
reason: "collect official/current external references only when configured local sources are insufficient",
|
|
110
|
+
}),
|
|
111
|
+
rolePoolEntry("planner", {
|
|
112
|
+
subagentType: "planner",
|
|
113
|
+
stages: ["task"],
|
|
114
|
+
activation: "when task sequencing, dependencies, and risk flags are in scope",
|
|
115
|
+
reason: "sequence implementation tasks, dependencies, ownership, and evidence hooks",
|
|
116
|
+
}),
|
|
117
|
+
rolePoolEntry("executor", {
|
|
118
|
+
subagentType: "executor",
|
|
119
|
+
stages: ["task", "coding"],
|
|
120
|
+
activation: "when implementation ownership, write scope, or scoped code changes are in scope",
|
|
121
|
+
reason: "implement or estimate scoped code ownership and write boundaries",
|
|
122
|
+
}),
|
|
123
|
+
rolePoolEntry("technical-writer", {
|
|
124
|
+
subagentType: "writer",
|
|
125
|
+
stages: ["proposal", "archive"],
|
|
126
|
+
activation: "when documentation structure, decision records, or handoff clarity are in scope",
|
|
127
|
+
reason: "preserve traceable docs, decisions, and reusable learning",
|
|
128
|
+
}),
|
|
129
|
+
rolePoolEntry("senior-engineering-reviewer", {
|
|
130
|
+
subagentType: "code-reviewer",
|
|
131
|
+
stages: ["review"],
|
|
132
|
+
activation: "when execution-path correctness, maintainability, regressions, or API compatibility need review",
|
|
133
|
+
reason: "review execution-path correctness, maintainability, regressions, and API boundaries",
|
|
134
|
+
}),
|
|
135
|
+
rolePoolEntry("ue-a11y-reviewer", {
|
|
136
|
+
subagentType: "designer",
|
|
137
|
+
stages: ["review"],
|
|
138
|
+
activation: "when UI, UX, accessibility, i18n, or visual behavior is touched",
|
|
139
|
+
reason: "review UX, accessibility, i18n, and visual impacts when UI is touched",
|
|
140
|
+
}),
|
|
141
|
+
rolePoolEntry("verifier", {
|
|
142
|
+
subagentType: "verifier",
|
|
143
|
+
required: true,
|
|
144
|
+
activation: "required",
|
|
145
|
+
reason: "verify stage outputs against source authority and command evidence",
|
|
146
|
+
}),
|
|
147
|
+
]),
|
|
148
|
+
};
|
|
149
|
+
const ROLE_CONTRACTS = {
|
|
150
|
+
"lead-integrator": {
|
|
151
|
+
owns: [
|
|
152
|
+
"stage plan, staffing, integration, source status, and evidence gate",
|
|
153
|
+
"shared config, manifest, indexes, and final handoff summary",
|
|
154
|
+
],
|
|
155
|
+
doesNotOwn: [
|
|
156
|
+
"treating lane output as automatic approval",
|
|
157
|
+
"inventing source authority or business acceptance without evidence",
|
|
158
|
+
],
|
|
159
|
+
outputs: [
|
|
160
|
+
".zsk/evidence/{scope}/{run}/integration-summary.md",
|
|
161
|
+
".zsk/raws/manifest.json when prepare snapshots change",
|
|
162
|
+
],
|
|
163
|
+
evidenceRequired: [
|
|
164
|
+
"integrated lane status with pass/blocker/waived reason for each role",
|
|
165
|
+
"confirmation that shared/global artifacts were updated only after lane validation",
|
|
166
|
+
],
|
|
167
|
+
forbiddenDecisions: [
|
|
168
|
+
"Do not accept subagent output without checking its evidence and write scope.",
|
|
169
|
+
"Do not decide uncertain source-to-lane mapping, platform identity, or authority without user confirmation.",
|
|
170
|
+
],
|
|
171
|
+
stopCondition: "All required lanes are integrated with evidence, or unresolved blockers are recorded for user/next-stage action.",
|
|
172
|
+
},
|
|
173
|
+
"product-owner": {
|
|
174
|
+
owns: ["business goals, product scope, priorities, requirement semantics, and acceptance meaning"],
|
|
175
|
+
doesNotOwn: ["backend implementation details", "technical feasibility signoff without architect/engineer evidence"],
|
|
176
|
+
outputs: [".zsk/evidence/{scope}/{run}/product-source-report.md"],
|
|
177
|
+
evidenceRequired: ["requirement/source IDs or paths behind each product claim", "open requirement gaps and acceptance risks"],
|
|
178
|
+
forbiddenDecisions: ["Do not rewrite technical design or implementation ownership.", "Do not turn weak/inferred evidence into accepted requirements."],
|
|
179
|
+
stopCondition: "Product facts, gaps, and acceptance risks are explicit and handed to lead-integrator.",
|
|
180
|
+
},
|
|
181
|
+
"business-analyst": {
|
|
182
|
+
owns: ["domain process, terminology, business rules, and edge cases"],
|
|
183
|
+
doesNotOwn: ["UI aesthetics", "implementation architecture"],
|
|
184
|
+
outputs: [".zsk/evidence/{scope}/{run}/business-rule-map.md"],
|
|
185
|
+
evidenceRequired: ["business rule source references", "ambiguity list for unresolved process or terminology conflicts"],
|
|
186
|
+
forbiddenDecisions: ["Do not choose product priority or implementation strategy.", "Do not resolve contradictory business rules silently."],
|
|
187
|
+
stopCondition: "Rules, edge cases, and unresolved ambiguities are documented for lead-integrator.",
|
|
188
|
+
},
|
|
189
|
+
architect: {
|
|
190
|
+
owns: ["system boundaries, module relationships, data flow, integration risks, and API/dependency boundaries"],
|
|
191
|
+
doesNotOwn: ["product priority decisions", "business acceptance signoff"],
|
|
192
|
+
outputs: [".zsk/evidence/{scope}/{run}/architecture-review.md"],
|
|
193
|
+
evidenceRequired: ["referenced files, modules, APIs, or diagrams behind architecture claims", "risk map with owner and verification hook"],
|
|
194
|
+
forbiddenDecisions: ["Do not override product acceptance or source truth.", "Do not widen implementation scope without lead-integrator handoff."],
|
|
195
|
+
stopCondition: "Architecture risks and boundary decisions are ready for integration or recorded as blockers.",
|
|
196
|
+
},
|
|
197
|
+
"backend-engineer": {
|
|
198
|
+
owns: ["backend repositories, API contracts, data model, auth/service boundaries, and backend snapshot validation"],
|
|
199
|
+
doesNotOwn: ["product acceptance wording", "design asset interpretation"],
|
|
200
|
+
outputs: [".zsk/evidence/{scope}/{run}/backend-source-report.md"],
|
|
201
|
+
evidenceRequired: ["repository/ref/path evidence", "API contract paths, data/auth risks, and missing backend source gaps"],
|
|
202
|
+
forbiddenDecisions: ["Do not infer product behavior from implementation alone.", "Do not write shared config or manifests directly from a lane."],
|
|
203
|
+
stopCondition: "Backend facts and gaps are reported with source references and no shared artifact edits pending.",
|
|
204
|
+
},
|
|
205
|
+
"frontend-engineer": {
|
|
206
|
+
owns: ["UI implementation, routing, state, component integration, and frontend framework constraints"],
|
|
207
|
+
doesNotOwn: ["source-of-truth product decisions", "backend/API correctness signoff"],
|
|
208
|
+
outputs: [".zsk/evidence/{scope}/{run}/frontend-impact-report.md"],
|
|
209
|
+
evidenceRequired: ["component/route/state file references", "UI implementation risks and validation hooks"],
|
|
210
|
+
forbiddenDecisions: ["Do not reinterpret acceptance criteria without product-owner handoff.", "Do not approve API behavior without backend evidence."],
|
|
211
|
+
stopCondition: "Frontend impact and validation needs are clear for lead-integrator.",
|
|
212
|
+
},
|
|
213
|
+
"design-specialist": {
|
|
214
|
+
owns: ["design assets, visual constraints, screen/state inventory, and design snapshot validation"],
|
|
215
|
+
doesNotOwn: ["backend/API correctness", "final product scope acceptance"],
|
|
216
|
+
outputs: [".zsk/evidence/{scope}/{run}/design-asset-report.md"],
|
|
217
|
+
evidenceRequired: ["design/prototype source references", "screen/state map and missing asset list"],
|
|
218
|
+
forbiddenDecisions: ["Do not infer implementation truth from design assets.", "Do not store auth state or exported private assets outside declared scope."],
|
|
219
|
+
stopCondition: "Design assets are mapped or blocked with provider/auth evidence.",
|
|
220
|
+
},
|
|
221
|
+
"ux-specialist": {
|
|
222
|
+
owns: ["user flows, interaction constraints, usability risks, and UX source validation"],
|
|
223
|
+
doesNotOwn: ["backend/API correctness", "business priority decisions"],
|
|
224
|
+
outputs: [".zsk/evidence/{scope}/{run}/ux-source-report.md"],
|
|
225
|
+
evidenceRequired: ["flow/source references", "usability risks and unresolved interaction questions"],
|
|
226
|
+
forbiddenDecisions: ["Do not approve product scope or technical design alone.", "Do not resolve UX/product conflicts silently."],
|
|
227
|
+
stopCondition: "UX facts, questions, and risks are ready for lead integration.",
|
|
228
|
+
},
|
|
229
|
+
"ue-a11y-reviewer": {
|
|
230
|
+
owns: ["UX, accessibility, visual, i18n, and frontend experience review when UI is touched"],
|
|
231
|
+
doesNotOwn: ["backend/API correctness", "security signoff"],
|
|
232
|
+
outputs: [".zsk/evidence/{scope}/{run}/ue-a11y-review.md"],
|
|
233
|
+
evidenceRequired: ["screens/components/scenarios reviewed", "accessibility and visual evidence or waived reason"],
|
|
234
|
+
forbiddenDecisions: ["Do not block or pass backend/security behavior.", "Do not invent visual requirements absent source evidence."],
|
|
235
|
+
stopCondition: "UX/a11y findings are reported or explicitly waived with reason.",
|
|
236
|
+
},
|
|
237
|
+
"qa-engineer": {
|
|
238
|
+
owns: ["test cases, regression matrix, acceptance scenarios, Playwright/runtime evidence, and test gaps"],
|
|
239
|
+
doesNotOwn: ["business signoff", "implementation design ownership"],
|
|
240
|
+
outputs: [".zsk/evidence/{scope}/{run}/qa-evidence-report.md"],
|
|
241
|
+
evidenceRequired: ["test command/scenario evidence", "acceptance coverage and missing-test gaps"],
|
|
242
|
+
forbiddenDecisions: ["Do not treat unrun tests as passing.", "Do not accept partial snapshots as complete evidence."],
|
|
243
|
+
stopCondition: "Coverage status is pass/fail/blocked with concrete evidence and residual risk.",
|
|
244
|
+
},
|
|
245
|
+
"security-reviewer": {
|
|
246
|
+
owns: ["auth, permissions, privacy, trust boundaries, data safety, and abuse cases"],
|
|
247
|
+
doesNotOwn: ["general style review", "product priority decisions"],
|
|
248
|
+
outputs: [".zsk/evidence/{scope}/{run}/security-review.md"],
|
|
249
|
+
evidenceRequired: ["auth/data flow references", "secret-handling and permission-risk assessment"],
|
|
250
|
+
forbiddenDecisions: ["Do not expose credential values in outputs.", "Do not approve unsafe credential storage or broad token scanning."],
|
|
251
|
+
stopCondition: "Security/data risks are cleared, waived with rationale, or blocked for user decision.",
|
|
252
|
+
},
|
|
253
|
+
"operations-engineer": {
|
|
254
|
+
owns: ["environment, CI/CD, deployment, configuration, rollback, and release risks"],
|
|
255
|
+
doesNotOwn: ["product semantics", "code-level correctness signoff without engineer evidence"],
|
|
256
|
+
outputs: [".zsk/evidence/{scope}/{run}/release-readiness.md"],
|
|
257
|
+
evidenceRequired: ["environment/config/CI references", "deployment and rollback risks"],
|
|
258
|
+
forbiddenDecisions: ["Do not change production or external systems from a planning lane.", "Do not assume env secrets are available."],
|
|
259
|
+
stopCondition: "Operational risks and verification hooks are ready for integration.",
|
|
260
|
+
},
|
|
261
|
+
researcher: {
|
|
262
|
+
owns: ["official docs, external references, market/regulatory evidence when relevant"],
|
|
263
|
+
doesNotOwn: ["converting external evidence into accepted product requirements alone"],
|
|
264
|
+
outputs: [".zsk/evidence/{scope}/{run}/research-brief.md"],
|
|
265
|
+
evidenceRequired: ["source links and dates when external facts are used", "adaptation notes separating fact from recommendation"],
|
|
266
|
+
forbiddenDecisions: ["Do not replace local configured sources with generic web evidence.", "Do not cite stale/current-sensitive facts without retrieval evidence."],
|
|
267
|
+
stopCondition: "Research facts and confidence/gaps are handed to lead-integrator.",
|
|
268
|
+
},
|
|
269
|
+
"technical-writer": {
|
|
270
|
+
owns: ["documentation structure, decision records, changelog, handoff clarity, and learning notes"],
|
|
271
|
+
doesNotOwn: ["technical correctness approval alone", "business acceptance alone"],
|
|
272
|
+
outputs: [".zsk/evidence/{scope}/{run}/documentation-handoff.md"],
|
|
273
|
+
evidenceRequired: ["changed docs list and source/evidence links", "no-update rationale when docs feedback is not needed"],
|
|
274
|
+
forbiddenDecisions: ["Do not polish away unresolved blockers.", "Do not create docs outside configured .zsk paths."],
|
|
275
|
+
stopCondition: "Docs are traceable, scoped, and ready for the next stage.",
|
|
276
|
+
},
|
|
277
|
+
"auth-fetch-agent": {
|
|
278
|
+
owns: ["Playwright auth state references, provider export flows, current-page acquisition, and fetch/snapshot validation"],
|
|
279
|
+
doesNotOwn: ["business content interpretation", "final source authority decisions"],
|
|
280
|
+
outputs: [
|
|
281
|
+
".zsk/evidence/prepare/{run}/adapter-results/*.json",
|
|
282
|
+
".zsk/raws/prepare/{lane}/{source}.md",
|
|
283
|
+
],
|
|
284
|
+
evidenceRequired: [
|
|
285
|
+
"adapter result with origin, strategy, validation, and snapshot hash",
|
|
286
|
+
"confirmation that login/chrome/empty pages were rejected",
|
|
287
|
+
"confirmation that credential material stayed outside versioned snapshots and evidence text",
|
|
288
|
+
],
|
|
289
|
+
forbiddenDecisions: [
|
|
290
|
+
"Do not infer platform type without configured origin evidence or successful probe evidence.",
|
|
291
|
+
"Do not overwrite a prior good snapshot with login, empty, redirect, or partial content.",
|
|
292
|
+
"Do not store cookies, tokens, passwords, or Playwright storageState in committed artifacts.",
|
|
293
|
+
],
|
|
294
|
+
stopCondition: "Snapshots are materialized with validation, or each blocked source has auth/provider evidence and a next-action hint.",
|
|
295
|
+
},
|
|
296
|
+
"senior-engineering-reviewer": {
|
|
297
|
+
owns: ["execution-path correctness, maintainability, regressions, API compatibility, and dependency boundaries"],
|
|
298
|
+
doesNotOwn: ["security/privacy signoff alone", "business acceptance signoff"],
|
|
299
|
+
outputs: [".zsk/evidence/{scope}/{run}/engineering-review.md"],
|
|
300
|
+
evidenceRequired: ["file/line or artifact references for findings", "test and residual-risk assessment"],
|
|
301
|
+
forbiddenDecisions: ["Do not approve without evidence adequacy review.", "Do not rewrite scope while reviewing."],
|
|
302
|
+
stopCondition: "Engineering findings are listed by severity or no-issue rationale is evidence-backed.",
|
|
303
|
+
},
|
|
304
|
+
verifier: {
|
|
305
|
+
owns: ["completion claim validation, evidence adequacy, and residual risk"],
|
|
306
|
+
doesNotOwn: ["implementation changes", "business acceptance decisions"],
|
|
307
|
+
outputs: [".zsk/evidence/{scope}/{run}/verification-report.md"],
|
|
308
|
+
evidenceRequired: ["fresh command/artifact evidence", "explicit pass/fail/blocked verdict against the claim"],
|
|
309
|
+
forbiddenDecisions: ["Do not claim PASS from stale or indirect evidence.", "Do not fix while acting as verifier."],
|
|
310
|
+
stopCondition: "The claim is proven, rejected, or blocked with linked evidence.",
|
|
311
|
+
},
|
|
312
|
+
planner: {
|
|
313
|
+
owns: ["task sequencing, dependencies, risk flags, and handoff shape"],
|
|
314
|
+
doesNotOwn: ["implementation edits", "final acceptance decisions"],
|
|
315
|
+
outputs: [".zsk/evidence/{scope}/{run}/planning-notes.md"],
|
|
316
|
+
evidenceRequired: ["dependency and risk rationale", "clear next legal stage"],
|
|
317
|
+
forbiddenDecisions: ["Do not skip stage gates.", "Do not assign shared writes without ownership boundaries."],
|
|
318
|
+
stopCondition: "The sequence and blockers are executable by the next role.",
|
|
319
|
+
},
|
|
320
|
+
executor: {
|
|
321
|
+
owns: ["scoped implementation or refactor work inside assigned files/modules"],
|
|
322
|
+
doesNotOwn: ["global plan rewrite", "independent final approval"],
|
|
323
|
+
outputs: [".zsk/evidence/{scope}/{run}/implementation-report.md"],
|
|
324
|
+
evidenceRequired: ["changed files and validation evidence", "blocker report for scope/write conflicts"],
|
|
325
|
+
forbiddenDecisions: ["Do not edit outside assigned write scope.", "Do not revert unknown user changes."],
|
|
326
|
+
stopCondition: "Assigned implementation slice is done with evidence or blocked for leader decision.",
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
async function readStaffingRolePool(target, config) {
|
|
330
|
+
const pools = [BUILTIN_ROLE_POOL];
|
|
331
|
+
const rolePoolPath = config.staffing?.rolePool;
|
|
332
|
+
if (rolePoolPath) {
|
|
333
|
+
try {
|
|
334
|
+
const content = await readFile(resolve(target, rolePoolPath), "utf8");
|
|
335
|
+
pools.push(parseRolePool(YAML.parse(content)));
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
if (rolePoolPath !== ".zsk/roles.yaml")
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (config.staffing?.roles) {
|
|
343
|
+
pools.push(parseRolePool({ roles: config.staffing.roles }));
|
|
344
|
+
}
|
|
345
|
+
return mergeRolePools(...pools);
|
|
346
|
+
}
|
|
347
|
+
function parseRolePool(value) {
|
|
348
|
+
const rolesValue = isRecord(value) ? value.roles : undefined;
|
|
349
|
+
if (!isRecord(rolesValue))
|
|
350
|
+
return { roles: {} };
|
|
351
|
+
const entries = [];
|
|
352
|
+
for (const [role, config] of Object.entries(rolesValue)) {
|
|
353
|
+
if (!isRecord(config))
|
|
354
|
+
continue;
|
|
355
|
+
entries.push(rolePoolEntry(role, {
|
|
356
|
+
subagentType: stringValue(config.subagentType),
|
|
357
|
+
reason: stringValue(config.reason),
|
|
358
|
+
stages: stringList(config.stages),
|
|
359
|
+
skills: stringList(config.skills),
|
|
360
|
+
lanes: stringList(config.lanes),
|
|
361
|
+
required: booleanValue(config.required),
|
|
362
|
+
remoteSources: booleanValue(config.remoteSources),
|
|
363
|
+
activation: stringValue(config.activation),
|
|
364
|
+
extraInputs: stringList(config.inputs),
|
|
365
|
+
writeScope: stringList(config.writeScope),
|
|
366
|
+
outputs: stringList(config.outputs),
|
|
367
|
+
evidenceRequired: stringList(config.evidenceRequired),
|
|
368
|
+
contract: {
|
|
369
|
+
owns: stringList(config.owns),
|
|
370
|
+
doesNotOwn: stringList(config.doesNotOwn),
|
|
371
|
+
forbiddenDecisions: stringList(config.forbiddenDecisions),
|
|
372
|
+
stopCondition: stringValue(config.stopCondition),
|
|
373
|
+
},
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
return { roles: Object.fromEntries(entries) };
|
|
377
|
+
}
|
|
378
|
+
function mergeRolePools(...pools) {
|
|
379
|
+
const roles = {};
|
|
380
|
+
for (const pool of pools) {
|
|
381
|
+
for (const [name, role] of Object.entries(pool.roles)) {
|
|
382
|
+
const existing = roles[name];
|
|
383
|
+
roles[name] = existing ? mergeRolePoolRole(existing, role) : { ...role };
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return { roles };
|
|
387
|
+
}
|
|
388
|
+
function mergeRolePoolRole(base, override) {
|
|
389
|
+
return {
|
|
390
|
+
role: base.role,
|
|
391
|
+
subagentType: override.subagentType ?? base.subagentType,
|
|
392
|
+
reason: override.reason ?? base.reason,
|
|
393
|
+
stages: unique([...base.stages, ...override.stages]),
|
|
394
|
+
skills: unique([...base.skills, ...override.skills]),
|
|
395
|
+
lanes: unique([...base.lanes, ...override.lanes]),
|
|
396
|
+
required: base.required || override.required,
|
|
397
|
+
remoteSources: base.remoteSources || override.remoteSources,
|
|
398
|
+
activation: override.activation ?? base.activation,
|
|
399
|
+
extraInputs: unique([...base.extraInputs, ...override.extraInputs]),
|
|
400
|
+
writeScope: unique([...base.writeScope, ...override.writeScope]),
|
|
401
|
+
outputs: unique([...base.outputs, ...override.outputs]),
|
|
402
|
+
evidenceRequired: unique([...base.evidenceRequired, ...override.evidenceRequired]),
|
|
403
|
+
contract: mergeContractOverrides(base.contract, override.contract),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function rolePoolEntry(role, config) {
|
|
407
|
+
return [role, {
|
|
408
|
+
role,
|
|
409
|
+
subagentType: config.subagentType,
|
|
410
|
+
reason: config.reason,
|
|
411
|
+
stages: normalizeList(config.stages ?? []),
|
|
412
|
+
skills: normalizeList(config.skills ?? []),
|
|
413
|
+
lanes: normalizeList(config.lanes ?? []),
|
|
414
|
+
required: config.required === true,
|
|
415
|
+
remoteSources: config.remoteSources === true,
|
|
416
|
+
activation: config.activation,
|
|
417
|
+
extraInputs: unique(config.extraInputs ?? []),
|
|
418
|
+
writeScope: unique(config.writeScope ?? []),
|
|
419
|
+
outputs: unique(config.outputs ?? []),
|
|
420
|
+
evidenceRequired: unique(config.evidenceRequired ?? []),
|
|
421
|
+
contract: config.contract,
|
|
422
|
+
}];
|
|
423
|
+
}
|
|
424
|
+
function seedFromRolePool(role, entries, overrides = {}) {
|
|
425
|
+
return {
|
|
426
|
+
role: role.role,
|
|
427
|
+
subagentType: role.subagentType ?? "executor",
|
|
428
|
+
reason: overrides.reason ?? role.reason ?? `configured ${role.role} role`,
|
|
429
|
+
entries,
|
|
430
|
+
activation: overrides.activation ?? role.activation,
|
|
431
|
+
extraInputs: role.extraInputs,
|
|
432
|
+
writeScope: unique([...(role.writeScope ?? []), ...(overrides.writeScope ?? [])]),
|
|
433
|
+
outputs: unique([...(role.outputs ?? []), ...(overrides.outputs ?? [])]),
|
|
434
|
+
evidenceRequired: unique([...(role.evidenceRequired ?? []), ...(overrides.evidenceRequired ?? [])]),
|
|
435
|
+
contract: role.contract,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function roleMatchesStageOrSkill(role, stage, skill) {
|
|
439
|
+
if (role.required)
|
|
440
|
+
return true;
|
|
441
|
+
const stageSlug = safeSlug(stage);
|
|
442
|
+
const skillSlug = safeSlug(skill);
|
|
443
|
+
return role.stages.includes(stageSlug) || role.skills.includes(skillSlug);
|
|
444
|
+
}
|
|
445
|
+
function rolesForLane(lane, rolePool) {
|
|
446
|
+
const laneSlug = safeSlug(lane);
|
|
447
|
+
const matches = Object.values(rolePool.roles).filter((role) => role.lanes.includes(laneSlug));
|
|
448
|
+
if (matches.length > 0)
|
|
449
|
+
return matches;
|
|
450
|
+
return [
|
|
451
|
+
rolePoolEntry(`${safeSlug(lane)}-specialist`, {
|
|
452
|
+
subagentType: "executor",
|
|
453
|
+
lanes: [lane],
|
|
454
|
+
activation: "when configured source is present",
|
|
455
|
+
reason: `collect, validate, and materialize prepare lane \`${lane}\` without changing source authority`,
|
|
456
|
+
})[1],
|
|
457
|
+
];
|
|
458
|
+
}
|
|
459
|
+
function summarizeStageGate(assessment, assessmentPath, markdownPath, waiverPath) {
|
|
460
|
+
return {
|
|
461
|
+
runId: assessment.runId,
|
|
462
|
+
stage: assessment.stage,
|
|
463
|
+
...(assessment.module ? { module: assessment.module } : {}),
|
|
464
|
+
status: assessment.status,
|
|
465
|
+
decision: assessment.decision,
|
|
466
|
+
score: assessment.score,
|
|
467
|
+
threshold: assessment.threshold,
|
|
468
|
+
blockers: assessment.blockers,
|
|
469
|
+
gaps: assessment.gaps,
|
|
470
|
+
assessmentPath,
|
|
471
|
+
markdownPath,
|
|
472
|
+
...(waiverPath ? { waiverPath } : {}),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function packetTimeoutPolicyFromConfig(config) {
|
|
476
|
+
const configured = config.staffing?.timeoutPolicy;
|
|
477
|
+
return {
|
|
478
|
+
heartbeatIntervalMs: positiveInteger(configured?.heartbeatIntervalMs) ?? DEFAULT_PACKET_TIMEOUT_POLICY.heartbeatIntervalMs,
|
|
479
|
+
staleAfterMs: positiveInteger(configured?.staleAfterMs) ?? DEFAULT_PACKET_TIMEOUT_POLICY.staleAfterMs,
|
|
480
|
+
maxAttempts: positiveInteger(configured?.maxAttempts) ?? DEFAULT_PACKET_TIMEOUT_POLICY.maxAttempts,
|
|
481
|
+
onTimeout: configured?.onTimeout ?? DEFAULT_PACKET_TIMEOUT_POLICY.onTimeout,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function positiveInteger(value) {
|
|
485
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
486
|
+
}
|
|
487
|
+
export async function writeStaffingPlan(target, config, opts = {}) {
|
|
488
|
+
const rolePool = await readStaffingRolePool(target, config);
|
|
489
|
+
const bundle = buildStaffingPlan(target, config, opts, rolePool);
|
|
490
|
+
const gateBundle = await writeGateAssessment(target, config, {
|
|
491
|
+
stage: bundle.plan.stage,
|
|
492
|
+
module: opts.module,
|
|
493
|
+
threshold: opts.gateThreshold ?? gateThresholdForStage(config, bundle.plan.stage),
|
|
494
|
+
acceptRisk: opts.acceptRisk,
|
|
495
|
+
});
|
|
496
|
+
bundle.plan = {
|
|
497
|
+
...bundle.plan,
|
|
498
|
+
...(opts.module ? { module: opts.module } : {}),
|
|
499
|
+
stageGate: summarizeStageGate(gateBundle.assessment, gateBundle.artifacts.jsonPath, gateBundle.artifacts.markdownPath, gateBundle.artifacts.waiverPath),
|
|
500
|
+
};
|
|
501
|
+
bundle.markdown = renderStaffingPlanMarkdown(bundle.plan, bundle.ledger.timeoutPolicy);
|
|
502
|
+
await mkdir(dirname(bundle.artifacts.jsonPath), { recursive: true });
|
|
503
|
+
await mkdir(bundle.artifacts.packetStatusDir, { recursive: true });
|
|
504
|
+
await writeFile(bundle.artifacts.jsonPath, `${JSON.stringify(bundle.plan, null, 2)}\n`, "utf8");
|
|
505
|
+
await writeFile(bundle.artifacts.markdownPath, bundle.markdown, "utf8");
|
|
506
|
+
await writeFile(bundle.artifacts.emitPath, bundle.emitJsonl, "utf8");
|
|
507
|
+
await writeFile(bundle.artifacts.ledgerPath, `${JSON.stringify(bundle.ledger, null, 2)}\n`, "utf8");
|
|
508
|
+
for (const packet of bundle.ledger.packets) {
|
|
509
|
+
await writeFile(resolve(target, packet.statusPath), `${JSON.stringify(packet, null, 2)}\n`, "utf8");
|
|
510
|
+
}
|
|
511
|
+
return bundle;
|
|
512
|
+
}
|
|
513
|
+
export function buildStaffingPlan(target, config, opts = {}, rolePool = BUILTIN_ROLE_POOL) {
|
|
514
|
+
const runId = opts.runId ?? createStaffingRunId();
|
|
515
|
+
const stage = normalizeStage(opts.stage ?? "prepare");
|
|
516
|
+
const skill = opts.skill ?? stage;
|
|
517
|
+
const surface = opts.surface ?? "auto";
|
|
518
|
+
const timeoutPolicy = packetTimeoutPolicyFromConfig(config);
|
|
519
|
+
const entries = flattenProjectSources(config.sources);
|
|
520
|
+
const seeds = seedRoles(stage, skill, entries, rolePool);
|
|
521
|
+
const orchestration = buildOrchestrationPlan(surface, stage, entries, seeds, config);
|
|
522
|
+
const roles = seeds.map((seed) => buildRole(seed, stage, skill, orchestration, config));
|
|
523
|
+
const dir = resolve(target, getWorkspacePath(config, "evidenceRoot"), "dispatch", runId);
|
|
524
|
+
const relativeDir = join(getWorkspacePath(config, "evidenceRoot"), "dispatch", runId);
|
|
525
|
+
const artifacts = {
|
|
526
|
+
runId,
|
|
527
|
+
dir,
|
|
528
|
+
jsonPath: join(dir, "staffing-plan.json"),
|
|
529
|
+
markdownPath: join(dir, "staffing-plan.md"),
|
|
530
|
+
emitPath: join(dir, "emit-packets.jsonl"),
|
|
531
|
+
ledgerPath: join(dir, "emit-ledger.json"),
|
|
532
|
+
packetStatusDir: join(dir, "packet-status"),
|
|
533
|
+
};
|
|
534
|
+
const relativeLedgerPath = join(relativeDir, "emit-ledger.json");
|
|
535
|
+
const relativePacketStatusDir = join(relativeDir, "packet-status");
|
|
536
|
+
const emitPackets = roles
|
|
537
|
+
.filter((role) => role.active)
|
|
538
|
+
.map((role, index) => emitPacketForRole(role, runId, index + 1, relativeLedgerPath, relativePacketStatusDir, timeoutPolicy));
|
|
539
|
+
const ledger = buildEmitLedger(runId, stage, skill, relativeLedgerPath, relativePacketStatusDir, emitPackets, timeoutPolicy);
|
|
540
|
+
const plan = {
|
|
541
|
+
runId,
|
|
542
|
+
stage,
|
|
543
|
+
skill,
|
|
544
|
+
...(opts.module ? { module: opts.module } : {}),
|
|
545
|
+
surface,
|
|
546
|
+
orchestration,
|
|
547
|
+
uncertaintyPolicy: [
|
|
548
|
+
"Do not promote uncertain source-to-lane correspondences without user confirmation.",
|
|
549
|
+
"Do not choose a provider-specific adapter from a URL guess when authority or completeness is unclear.",
|
|
550
|
+
"Do not overwrite a useful snapshot with an empty, login, redirect, or partial capture.",
|
|
551
|
+
"Do not store credentials, cookies, tokens, or Playwright storageState in raws, docs, evidence, or committed artifacts.",
|
|
552
|
+
"Do not run provider, design, repository, or work-item fetches from generic global credential names; credentials must be explicitly bound to the source or auth profile.",
|
|
553
|
+
],
|
|
554
|
+
roles,
|
|
555
|
+
emitPackets,
|
|
556
|
+
};
|
|
557
|
+
return {
|
|
558
|
+
artifacts,
|
|
559
|
+
plan,
|
|
560
|
+
markdown: renderStaffingPlanMarkdown(plan, timeoutPolicy),
|
|
561
|
+
emitJsonl: emitPackets.map((packet) => JSON.stringify(packet)).join("\n") + "\n",
|
|
562
|
+
ledger,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
export async function inspectStaffingRun(target, config, opts = {}) {
|
|
566
|
+
const dispatchRoot = resolve(target, getWorkspacePath(config, "evidenceRoot"), "dispatch");
|
|
567
|
+
const runId = opts.runId ?? await latestDispatchRunId(dispatchRoot);
|
|
568
|
+
const ledgerPath = join(dispatchRoot, runId, "emit-ledger.json");
|
|
569
|
+
const ledger = JSON.parse(await readFile(ledgerPath, "utf8"));
|
|
570
|
+
const now = opts.now ?? new Date();
|
|
571
|
+
const packets = [];
|
|
572
|
+
for (const packet of ledger.packets) {
|
|
573
|
+
const statusPath = resolve(target, packet.statusPath);
|
|
574
|
+
const current = JSON.parse(await readFile(statusPath, "utf8"));
|
|
575
|
+
const next = shouldMarkPacketStale(current, now) ? markPacketStale(current, now) : current;
|
|
576
|
+
packets.push(next);
|
|
577
|
+
if (next !== current)
|
|
578
|
+
await writeFile(statusPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
579
|
+
}
|
|
580
|
+
const nextLedger = { ...ledger, packets };
|
|
581
|
+
await writeFile(ledgerPath, `${JSON.stringify(nextLedger, null, 2)}\n`, "utf8");
|
|
582
|
+
return {
|
|
583
|
+
runId,
|
|
584
|
+
ledgerPath,
|
|
585
|
+
total: packets.length,
|
|
586
|
+
stale: packets.filter((packet) => packet.status === "stale").length,
|
|
587
|
+
pending: packets.filter((packet) => packet.status === "pending").length,
|
|
588
|
+
running: packets.filter((packet) => packet.status === "running").length,
|
|
589
|
+
completed: packets.filter((packet) => packet.status === "completed").length,
|
|
590
|
+
blocked: packets.filter((packet) => packet.status === "blocked").length,
|
|
591
|
+
failed: packets.filter((packet) => packet.status === "failed").length,
|
|
592
|
+
waived: packets.filter((packet) => packet.status === "waived").length,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
async function latestDispatchRunId(dispatchRoot) {
|
|
596
|
+
const entries = (await readdir(dispatchRoot, { withFileTypes: true }))
|
|
597
|
+
.filter((entry) => entry.isDirectory())
|
|
598
|
+
.map((entry) => entry.name)
|
|
599
|
+
.sort();
|
|
600
|
+
const latest = entries.at(-1);
|
|
601
|
+
if (!latest)
|
|
602
|
+
throw new Error(`No dispatch runs found under ${dispatchRoot}`);
|
|
603
|
+
return latest;
|
|
604
|
+
}
|
|
605
|
+
function shouldMarkPacketStale(packet, now) {
|
|
606
|
+
if (!["pending", "running"].includes(packet.status))
|
|
607
|
+
return false;
|
|
608
|
+
const deadline = Date.parse(packet.deadlineAt);
|
|
609
|
+
return Number.isFinite(deadline) && now.getTime() > deadline;
|
|
610
|
+
}
|
|
611
|
+
function markPacketStale(packet, now) {
|
|
612
|
+
return {
|
|
613
|
+
...packet,
|
|
614
|
+
status: "stale",
|
|
615
|
+
updatedAt: now.toISOString(),
|
|
616
|
+
blocker: "No heartbeat was recorded before the packet deadline; route this lane through leader-sequential fallback or re-emit with a new packet.",
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function seedRoles(stage, skill, entries, rolePool) {
|
|
620
|
+
const seeds = new Map();
|
|
621
|
+
const poolRoles = Object.values(rolePool.roles);
|
|
622
|
+
for (const role of poolRoles) {
|
|
623
|
+
if (!role.required && !roleMatchesStageOrSkill(role, stage, skill))
|
|
624
|
+
continue;
|
|
625
|
+
addSeed(seeds, seedFromRolePool(role, []));
|
|
626
|
+
}
|
|
627
|
+
if (stage === "prepare" || skill === "prepare") {
|
|
628
|
+
for (const entry of entries) {
|
|
629
|
+
const lane = entry.rawLane ?? entry.segments[0] ?? "source";
|
|
630
|
+
const mapped = rolesForLane(lane, rolePool);
|
|
631
|
+
for (const role of mapped) {
|
|
632
|
+
addSeed(seeds, seedFromRolePool(role, [entry], {
|
|
633
|
+
reason: role.reason || `collect, validate, and materialize prepare lane \`${lane}\` without changing source authority`,
|
|
634
|
+
activation: role.activation ?? "when configured source is present",
|
|
635
|
+
}));
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
const remoteEntries = entries.filter((entry) => inferSourceOrigin(entry.source).remote);
|
|
639
|
+
if (remoteEntries.length > 0) {
|
|
640
|
+
for (const role of poolRoles.filter((item) => item.remoteSources && roleMatchesStageOrSkill(item, stage, skill))) {
|
|
641
|
+
addSeed(seeds, seedFromRolePool(role, remoteEntries, {
|
|
642
|
+
activation: role.activation ?? "when remote/provider source is present",
|
|
643
|
+
writeScope: [
|
|
644
|
+
".zsk/raws/prepare/**",
|
|
645
|
+
".zsk/playwright/.auth/**",
|
|
646
|
+
".zsk/evidence/prepare/**",
|
|
647
|
+
...role.writeScope,
|
|
648
|
+
],
|
|
649
|
+
outputs: [
|
|
650
|
+
".zsk/evidence/prepare/{run}/adapter-results/*.json",
|
|
651
|
+
".zsk/raws/prepare/{lane}/{source}.md",
|
|
652
|
+
...role.outputs,
|
|
653
|
+
],
|
|
654
|
+
evidenceRequired: [
|
|
655
|
+
"adapter result with origin, strategy, validation, and snapshot hash",
|
|
656
|
+
"confirmation that login/chrome/empty pages were rejected",
|
|
657
|
+
"confirmation that storageState or credentials were not copied into versioned source snapshots",
|
|
658
|
+
...role.evidenceRequired,
|
|
659
|
+
],
|
|
660
|
+
}));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return [...seeds.values()];
|
|
665
|
+
}
|
|
666
|
+
function addSeed(seeds, seed) {
|
|
667
|
+
const existing = seeds.get(seed.role);
|
|
668
|
+
if (!existing) {
|
|
669
|
+
seeds.set(seed.role, {
|
|
670
|
+
...seed,
|
|
671
|
+
entries: [...seed.entries],
|
|
672
|
+
activation: seed.activation,
|
|
673
|
+
extraInputs: [...(seed.extraInputs ?? [])],
|
|
674
|
+
writeScope: [...(seed.writeScope ?? [])],
|
|
675
|
+
outputs: [...(seed.outputs ?? [])],
|
|
676
|
+
evidenceRequired: [...(seed.evidenceRequired ?? [])],
|
|
677
|
+
contract: seed.contract,
|
|
678
|
+
});
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
existing.entries.push(...seed.entries);
|
|
682
|
+
if (!existing.activation && seed.activation)
|
|
683
|
+
existing.activation = seed.activation;
|
|
684
|
+
existing.extraInputs?.push(...(seed.extraInputs ?? []));
|
|
685
|
+
existing.writeScope?.push(...(seed.writeScope ?? []));
|
|
686
|
+
existing.outputs?.push(...(seed.outputs ?? []));
|
|
687
|
+
existing.evidenceRequired?.push(...(seed.evidenceRequired ?? []));
|
|
688
|
+
existing.contract = mergeContractOverrides(existing.contract, seed.contract);
|
|
689
|
+
}
|
|
690
|
+
function buildOrchestrationPlan(surface, stage, entries, seeds, config) {
|
|
691
|
+
const requestedSurface = normalizeSurface(surface);
|
|
692
|
+
const explicitSurface = requestedSurface !== "auto";
|
|
693
|
+
const mode = normalizeMode(config.mode);
|
|
694
|
+
const scale = normalizeScale(config.scale);
|
|
695
|
+
const policy = dispatchPolicyFor(mode, scale);
|
|
696
|
+
const lanes = new Set(entries.map((entry) => entry.rawLane ?? entry.segments[0] ?? "source"));
|
|
697
|
+
const remoteCount = entries.filter((entry) => inferSourceOrigin(entry.source).remote).length;
|
|
698
|
+
const activeSeedCount = seeds.filter((seed) => seedIsActive(seed, stage)).length;
|
|
699
|
+
const stageParallelNeed = (policy.strictReview && ["review", "verify"].includes(stage)) ||
|
|
700
|
+
(["prepare", "design"].includes(stage) && activeSeedCount > 2);
|
|
701
|
+
const hasIndependentLanes = lanes.size > 1 ||
|
|
702
|
+
remoteCount > 0 ||
|
|
703
|
+
stageParallelNeed ||
|
|
704
|
+
activeSeedCount > 4;
|
|
705
|
+
const dispatchRecommended = requestedSurface !== "sequential" &&
|
|
706
|
+
hasIndependentLanes &&
|
|
707
|
+
(explicitSurface || policy.autoDispatch);
|
|
708
|
+
const selectedSurface = requestedSurface === "auto" && !dispatchRecommended ? "sequential" : requestedSurface;
|
|
709
|
+
const maxParallelLanes = requestedSurface === "sequential"
|
|
710
|
+
? 1
|
|
711
|
+
: Math.min(policy.maxParallelLanes, Math.max(1, activeSeedCount - 1));
|
|
712
|
+
return {
|
|
713
|
+
requestedSurface,
|
|
714
|
+
selectedSurface,
|
|
715
|
+
mode,
|
|
716
|
+
scale,
|
|
717
|
+
dispatchRecommended,
|
|
718
|
+
laneIndependence: dispatchRecommended ? "parallel-capable" : "sequential",
|
|
719
|
+
reason: dispatchRecommended
|
|
720
|
+
? `mode=${mode}, scale=${scale}: stage has independent expert lanes or remote/provider acquisition work`
|
|
721
|
+
: `mode=${mode}, scale=${scale}: ${policy.sequentialReason}`,
|
|
722
|
+
fallbackOrder: [
|
|
723
|
+
"native Codex subagents for bounded in-session parallel lanes",
|
|
724
|
+
"omx emit/team/ultrawork when durable or tmux-based coordination is available",
|
|
725
|
+
"leader-sequential role simulation with the same lane contract",
|
|
726
|
+
],
|
|
727
|
+
maxParallelLanes,
|
|
728
|
+
overDispatchGuards: [
|
|
729
|
+
"Do not dispatch inactive lanes without configured source, touched surface, or explicit stage need.",
|
|
730
|
+
"Do not parallelize lanes that edit the same shared config, manifest, index, or module stage document.",
|
|
731
|
+
"Do not use subagents for a compact single-lane task when leader-sequential execution is clearer.",
|
|
732
|
+
"Do not let provider lanes write shared artifacts; lead-integrator owns integration.",
|
|
733
|
+
"Apply mode/scale policy before activating parallel lanes; explicit surface flags are the only override for solo/tiny scales.",
|
|
734
|
+
],
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function normalizeMode(value) {
|
|
738
|
+
return safeSlug(value ?? "standard") || "standard";
|
|
739
|
+
}
|
|
740
|
+
function normalizeScale(value) {
|
|
741
|
+
return safeSlug(value ?? "auto") || "auto";
|
|
742
|
+
}
|
|
743
|
+
function dispatchPolicyFor(mode, scale) {
|
|
744
|
+
const strictReview = ["strict", "incident", "review-heavy"].includes(mode);
|
|
745
|
+
if (scale === "solo" || scale === "tiny") {
|
|
746
|
+
return {
|
|
747
|
+
autoDispatch: false,
|
|
748
|
+
maxParallelLanes: 1,
|
|
749
|
+
strictReview,
|
|
750
|
+
sequentialReason: "solo/tiny scale keeps the common path leader-sequential unless the caller explicitly selects a dispatch surface",
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
if (scale === "small") {
|
|
754
|
+
return {
|
|
755
|
+
autoDispatch: true,
|
|
756
|
+
maxParallelLanes: 2,
|
|
757
|
+
strictReview,
|
|
758
|
+
sequentialReason: "small scale only parallelizes when independent evidence-backed lanes exist",
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
if (scale === "standard") {
|
|
762
|
+
return {
|
|
763
|
+
autoDispatch: true,
|
|
764
|
+
maxParallelLanes: 4,
|
|
765
|
+
strictReview,
|
|
766
|
+
sequentialReason: "standard scale found no independent lane that needs parallel dispatch",
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
if (scale === "large") {
|
|
770
|
+
return {
|
|
771
|
+
autoDispatch: true,
|
|
772
|
+
maxParallelLanes: 6,
|
|
773
|
+
strictReview,
|
|
774
|
+
sequentialReason: "large scale found no independent lane that needs parallel dispatch",
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
return {
|
|
778
|
+
autoDispatch: true,
|
|
779
|
+
maxParallelLanes: 6,
|
|
780
|
+
strictReview,
|
|
781
|
+
sequentialReason: "auto scale found no independent lane that needs parallel dispatch",
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
function buildRole(seed, stage, skill, orchestration, config) {
|
|
785
|
+
const active = seedIsActive(seed, stage);
|
|
786
|
+
const contract = contractForSeed(seed);
|
|
787
|
+
const route = active ? chooseRoute(orchestration, seed.subagentType) : "not-scheduled";
|
|
788
|
+
const inputs = unique([
|
|
789
|
+
...sourceInputs(seed.entries),
|
|
790
|
+
...(seed.extraInputs ?? []),
|
|
791
|
+
getWorkspacePath(config, "config"),
|
|
792
|
+
join(getWorkspacePath(config, "docsRoot"), "PROJECT-CONFIG.md"),
|
|
793
|
+
join(getWorkspacePath(config, "docsRoot"), "SYSTEM-SPEC.md"),
|
|
794
|
+
]);
|
|
795
|
+
const writeScope = unique([
|
|
796
|
+
...(seed.writeScope ?? []),
|
|
797
|
+
...sourceWriteScopes(seed.entries),
|
|
798
|
+
getWorkspacePath(config, "evidenceRoot") + "/**",
|
|
799
|
+
]);
|
|
800
|
+
const outputs = unique([
|
|
801
|
+
...contract.outputs,
|
|
802
|
+
...(seed.outputs ?? []),
|
|
803
|
+
...seed.entries.map((entry) => entry.source.snapshot ?? entry.source.output).filter(isString),
|
|
804
|
+
]);
|
|
805
|
+
const evidenceRequired = unique([
|
|
806
|
+
...contract.evidenceRequired,
|
|
807
|
+
...(seed.evidenceRequired ?? []),
|
|
808
|
+
"PROJECT-CONFIG.md and SYSTEM-SPEC.md constraints checked before stage output",
|
|
809
|
+
"explicit changed/unchanged/blocker result",
|
|
810
|
+
"fresh command or artifact evidence before completion claim",
|
|
811
|
+
]);
|
|
812
|
+
return {
|
|
813
|
+
role: seed.role,
|
|
814
|
+
subagentType: seed.subagentType,
|
|
815
|
+
stage,
|
|
816
|
+
skill,
|
|
817
|
+
active,
|
|
818
|
+
route,
|
|
819
|
+
activation: seed.activation ?? "required for this stage",
|
|
820
|
+
reason: seed.reason,
|
|
821
|
+
owns: contract.owns,
|
|
822
|
+
doesNotOwn: contract.doesNotOwn,
|
|
823
|
+
inputs,
|
|
824
|
+
writeScope,
|
|
825
|
+
outputs,
|
|
826
|
+
evidenceRequired,
|
|
827
|
+
forbiddenDecisions: contract.forbiddenDecisions,
|
|
828
|
+
handoffTo: seed.role === "lead-integrator" ? "verifier" : "lead-integrator",
|
|
829
|
+
stopCondition: contract.stopCondition,
|
|
830
|
+
mustAskWhen: [
|
|
831
|
+
"source ownership, lane mapping, or platform identity is uncertain",
|
|
832
|
+
"remote auth is required and no approved reusable session exists",
|
|
833
|
+
"multiple sources conflict about the same product or implementation fact",
|
|
834
|
+
"a fetch returns login chrome, partial content, or ambiguous page content",
|
|
835
|
+
"a migration would move, delete, overwrite, or de-authorize existing snapshots",
|
|
836
|
+
"PROJECT-CONFIG.md or SYSTEM-SPEC.md conflicts with requested stage output",
|
|
837
|
+
],
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
function chooseRoute(orchestration, subagentType) {
|
|
841
|
+
if (!orchestration.dispatchRecommended)
|
|
842
|
+
return "leader-sequential";
|
|
843
|
+
if (orchestration.selectedSurface === "native")
|
|
844
|
+
return `native:${subagentType}`;
|
|
845
|
+
if (orchestration.selectedSurface === "omx")
|
|
846
|
+
return `omx:emit:${subagentType}`;
|
|
847
|
+
if (orchestration.selectedSurface === "sequential")
|
|
848
|
+
return "leader-sequential";
|
|
849
|
+
return `auto:emit-compatible:${subagentType}`;
|
|
850
|
+
}
|
|
851
|
+
function contractForSeed(seed) {
|
|
852
|
+
return mergeRoleContract(contractForRole(seed.role, seed.subagentType), seed.contract);
|
|
853
|
+
}
|
|
854
|
+
function contractForRole(role, subagentType) {
|
|
855
|
+
if (ROLE_CONTRACTS[role])
|
|
856
|
+
return ROLE_CONTRACTS[role];
|
|
857
|
+
if (ROLE_CONTRACTS[subagentType])
|
|
858
|
+
return ROLE_CONTRACTS[subagentType];
|
|
859
|
+
return {
|
|
860
|
+
owns: ["the configured lane facts, gaps, outputs, and evidence for this role"],
|
|
861
|
+
doesNotOwn: ["shared config, manifest, indexes, global scope, or final approval"],
|
|
862
|
+
outputs: [".zsk/evidence/{scope}/{run}/lane-report.md"],
|
|
863
|
+
evidenceRequired: ["source references and blocker list for this lane"],
|
|
864
|
+
forbiddenDecisions: [
|
|
865
|
+
"Do not decide source authority, lane mapping, or business acceptance without lead-integrator confirmation.",
|
|
866
|
+
"Do not write outside the assigned lane scope.",
|
|
867
|
+
],
|
|
868
|
+
stopCondition: "Lane facts and gaps are reported with evidence, or a blocker is handed to lead-integrator.",
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
function mergeRoleContract(base, override) {
|
|
872
|
+
if (!override)
|
|
873
|
+
return base;
|
|
874
|
+
return {
|
|
875
|
+
owns: override.owns && override.owns.length > 0 ? override.owns : base.owns,
|
|
876
|
+
doesNotOwn: override.doesNotOwn && override.doesNotOwn.length > 0 ? override.doesNotOwn : base.doesNotOwn,
|
|
877
|
+
outputs: override.outputs && override.outputs.length > 0 ? unique([...base.outputs, ...override.outputs]) : base.outputs,
|
|
878
|
+
evidenceRequired: override.evidenceRequired && override.evidenceRequired.length > 0
|
|
879
|
+
? unique([...base.evidenceRequired, ...override.evidenceRequired])
|
|
880
|
+
: base.evidenceRequired,
|
|
881
|
+
forbiddenDecisions: override.forbiddenDecisions && override.forbiddenDecisions.length > 0
|
|
882
|
+
? unique([...base.forbiddenDecisions, ...override.forbiddenDecisions])
|
|
883
|
+
: base.forbiddenDecisions,
|
|
884
|
+
stopCondition: override.stopCondition ?? base.stopCondition,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
function mergeContractOverrides(base, override) {
|
|
888
|
+
if (!base)
|
|
889
|
+
return override;
|
|
890
|
+
if (!override)
|
|
891
|
+
return base;
|
|
892
|
+
return {
|
|
893
|
+
owns: override.owns && override.owns.length > 0 ? override.owns : base.owns,
|
|
894
|
+
doesNotOwn: override.doesNotOwn && override.doesNotOwn.length > 0 ? override.doesNotOwn : base.doesNotOwn,
|
|
895
|
+
outputs: unique([...(base.outputs ?? []), ...(override.outputs ?? [])]),
|
|
896
|
+
evidenceRequired: unique([...(base.evidenceRequired ?? []), ...(override.evidenceRequired ?? [])]),
|
|
897
|
+
forbiddenDecisions: unique([...(base.forbiddenDecisions ?? []), ...(override.forbiddenDecisions ?? [])]),
|
|
898
|
+
stopCondition: override.stopCondition ?? base.stopCondition,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function seedIsActive(seed, stage) {
|
|
902
|
+
if (seed.entries.length > 0)
|
|
903
|
+
return true;
|
|
904
|
+
if (seed.activation === "required")
|
|
905
|
+
return true;
|
|
906
|
+
if (seed.activation?.startsWith("optional"))
|
|
907
|
+
return false;
|
|
908
|
+
if ((stage === "prepare" || stage === "design") && seed.activation?.startsWith("when"))
|
|
909
|
+
return false;
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
function normalizeSurface(value) {
|
|
913
|
+
if (value === "native" || value === "omx" || value === "sequential")
|
|
914
|
+
return value;
|
|
915
|
+
return "auto";
|
|
916
|
+
}
|
|
917
|
+
function sourceInputs(entries) {
|
|
918
|
+
return entries.flatMap((entry) => {
|
|
919
|
+
const origin = inferSourceOrigin(entry.source);
|
|
920
|
+
return [
|
|
921
|
+
entry.path,
|
|
922
|
+
origin.ref ? `origin:${origin.ref}` : "",
|
|
923
|
+
entry.source.snapshot ? `snapshot:${entry.source.snapshot}` : "",
|
|
924
|
+
].filter(isString);
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
function sourceWriteScopes(entries) {
|
|
928
|
+
return entries
|
|
929
|
+
.map((entry) => {
|
|
930
|
+
if (entry.source.snapshot)
|
|
931
|
+
return entry.source.snapshot;
|
|
932
|
+
if (entry.rawLane)
|
|
933
|
+
return `.zsk/raws/prepare/${entry.rawLane}/**`;
|
|
934
|
+
return "";
|
|
935
|
+
})
|
|
936
|
+
.filter(isString);
|
|
937
|
+
}
|
|
938
|
+
function emitPacketForRole(role, runId, sequence, ledgerPath, packetStatusDir, timeoutPolicy) {
|
|
939
|
+
const packetId = `${String(sequence).padStart(3, "0")}-${safeSlug(role.role)}`;
|
|
940
|
+
const statusPath = join(packetStatusDir, `${packetId}.json`);
|
|
941
|
+
const createdAt = new Date().toISOString();
|
|
942
|
+
const deadlineAt = new Date(Date.parse(createdAt) + timeoutPolicy.staleAfterMs).toISOString();
|
|
943
|
+
return {
|
|
944
|
+
event: "zsk.subagent.request",
|
|
945
|
+
version: 1,
|
|
946
|
+
payload: {
|
|
947
|
+
runId,
|
|
948
|
+
packetId,
|
|
949
|
+
stage: role.stage,
|
|
950
|
+
skill: role.skill,
|
|
951
|
+
role: role.role,
|
|
952
|
+
subagentType: role.subagentType,
|
|
953
|
+
active: role.active,
|
|
954
|
+
route: role.route,
|
|
955
|
+
activation: role.activation,
|
|
956
|
+
prompt: [
|
|
957
|
+
`Execute the ${role.stage} stage as ${role.role}.`,
|
|
958
|
+
`Activation: ${role.activation}.`,
|
|
959
|
+
"Stay inside the declared write scope.",
|
|
960
|
+
"Check PROJECT-CONFIG.md and SYSTEM-SPEC.md before deciding stage output.",
|
|
961
|
+
"Report blockers instead of silently deciding uncertain source facts.",
|
|
962
|
+
].join(" "),
|
|
963
|
+
inputs: role.inputs,
|
|
964
|
+
writeScope: role.writeScope,
|
|
965
|
+
outputs: role.outputs,
|
|
966
|
+
evidenceRequired: role.evidenceRequired,
|
|
967
|
+
forbiddenDecisions: role.forbiddenDecisions,
|
|
968
|
+
handoffTo: role.handoffTo,
|
|
969
|
+
stopCondition: role.stopCondition,
|
|
970
|
+
constraints: role.mustAskWhen,
|
|
971
|
+
durability: {
|
|
972
|
+
ledgerPath,
|
|
973
|
+
statusPath,
|
|
974
|
+
status: "pending",
|
|
975
|
+
attempt: 1,
|
|
976
|
+
createdAt,
|
|
977
|
+
heartbeatIntervalMs: timeoutPolicy.heartbeatIntervalMs,
|
|
978
|
+
staleAfterMs: timeoutPolicy.staleAfterMs,
|
|
979
|
+
deadlineAt,
|
|
980
|
+
maxAttempts: timeoutPolicy.maxAttempts,
|
|
981
|
+
onTimeout: timeoutPolicy.onTimeout,
|
|
982
|
+
resumeHint: "Update this packet status with evidence before the lead-integrator claims the lane is complete.",
|
|
983
|
+
},
|
|
984
|
+
},
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
function buildEmitLedger(runId, stage, skill, ledgerPath, packetStatusDir, packets, timeoutPolicy) {
|
|
988
|
+
return {
|
|
989
|
+
version: 1,
|
|
990
|
+
runId,
|
|
991
|
+
stage,
|
|
992
|
+
skill,
|
|
993
|
+
ledgerPath,
|
|
994
|
+
packetStatusDir,
|
|
995
|
+
timeoutPolicy,
|
|
996
|
+
packets: packets.map((packet) => ({
|
|
997
|
+
packetId: packet.payload.packetId,
|
|
998
|
+
runId,
|
|
999
|
+
stage,
|
|
1000
|
+
skill,
|
|
1001
|
+
role: packet.payload.role,
|
|
1002
|
+
subagentType: packet.payload.subagentType,
|
|
1003
|
+
route: packet.payload.route,
|
|
1004
|
+
status: "pending",
|
|
1005
|
+
attempt: packet.payload.durability.attempt,
|
|
1006
|
+
createdAt: packet.payload.durability.createdAt,
|
|
1007
|
+
updatedAt: packet.payload.durability.createdAt,
|
|
1008
|
+
lastHeartbeatAt: packet.payload.durability.createdAt,
|
|
1009
|
+
deadlineAt: packet.payload.durability.deadlineAt,
|
|
1010
|
+
staleAfterMs: packet.payload.durability.staleAfterMs,
|
|
1011
|
+
maxAttempts: packet.payload.durability.maxAttempts,
|
|
1012
|
+
fallbackRoute: "leader-sequential",
|
|
1013
|
+
statusPath: packet.payload.durability.statusPath,
|
|
1014
|
+
evidence: [],
|
|
1015
|
+
})),
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
function renderStaffingPlanMarkdown(plan, timeoutPolicy) {
|
|
1019
|
+
return [
|
|
1020
|
+
"# Staffing Plan",
|
|
1021
|
+
"",
|
|
1022
|
+
`- Stage: \`${plan.stage}\``,
|
|
1023
|
+
`- Skill: \`${plan.skill}\``,
|
|
1024
|
+
...(plan.module ? [`- Module: \`${plan.module}\``] : []),
|
|
1025
|
+
`- Surface: \`${plan.surface}\``,
|
|
1026
|
+
`- Roles: ${plan.roles.length}`,
|
|
1027
|
+
`- Active emit lanes: ${plan.emitPackets.length}`,
|
|
1028
|
+
...(plan.stageGate ? [
|
|
1029
|
+
`- Stage gate: \`${plan.stageGate.status}\` (${plan.stageGate.score}/10, decision \`${plan.stageGate.decision}\`)`,
|
|
1030
|
+
`- Stage gate evidence: \`${plan.stageGate.assessmentPath}\``,
|
|
1031
|
+
] : []),
|
|
1032
|
+
"",
|
|
1033
|
+
"## Orchestration",
|
|
1034
|
+
"",
|
|
1035
|
+
`- Requested surface: \`${plan.orchestration.requestedSurface}\``,
|
|
1036
|
+
`- Selected surface: \`${plan.orchestration.selectedSurface}\``,
|
|
1037
|
+
`- Mode: \`${plan.orchestration.mode}\``,
|
|
1038
|
+
`- Scale: \`${plan.orchestration.scale}\``,
|
|
1039
|
+
`- Dispatch recommended: ${plan.orchestration.dispatchRecommended ? "yes" : "no"}`,
|
|
1040
|
+
`- Lane independence: \`${plan.orchestration.laneIndependence}\``,
|
|
1041
|
+
`- Reason: ${plan.orchestration.reason}`,
|
|
1042
|
+
`- Max parallel lanes: ${plan.orchestration.maxParallelLanes}`,
|
|
1043
|
+
`- Durable packet ledger: \`.zsk/evidence/dispatch/${plan.runId}/emit-ledger.json\``,
|
|
1044
|
+
`- Packet timeout policy: heartbeat every ${timeoutPolicy.heartbeatIntervalMs}ms; stale after ${timeoutPolicy.staleAfterMs}ms; max attempts ${timeoutPolicy.maxAttempts}; fallback ${timeoutPolicy.onTimeout}`,
|
|
1045
|
+
"",
|
|
1046
|
+
"### Fallback Order",
|
|
1047
|
+
"",
|
|
1048
|
+
...plan.orchestration.fallbackOrder.map((item) => `- ${item}`),
|
|
1049
|
+
"",
|
|
1050
|
+
"### Over-Dispatch Guards",
|
|
1051
|
+
"",
|
|
1052
|
+
...plan.orchestration.overDispatchGuards.map((item) => `- ${item}`),
|
|
1053
|
+
"",
|
|
1054
|
+
"## Uncertainty Policy",
|
|
1055
|
+
"",
|
|
1056
|
+
...plan.uncertaintyPolicy.map((item) => `- ${item}`),
|
|
1057
|
+
"",
|
|
1058
|
+
"## Roles",
|
|
1059
|
+
"",
|
|
1060
|
+
...plan.roles.flatMap((role) => [
|
|
1061
|
+
`### ${role.role}`,
|
|
1062
|
+
"",
|
|
1063
|
+
`- Subagent type: \`${role.subagentType}\``,
|
|
1064
|
+
`- Active: ${role.active ? "yes" : "no"}`,
|
|
1065
|
+
`- Activation: ${role.activation}`,
|
|
1066
|
+
`- Route: \`${role.route}\``,
|
|
1067
|
+
`- Reason: ${role.reason}`,
|
|
1068
|
+
`- Owns: ${role.owns.map((item) => `\`${item}\``).join(", ")}`,
|
|
1069
|
+
`- Does not own: ${role.doesNotOwn.map((item) => `\`${item}\``).join(", ")}`,
|
|
1070
|
+
`- Inputs: ${role.inputs.length === 0 ? "none" : role.inputs.map((item) => `\`${item}\``).join(", ")}`,
|
|
1071
|
+
`- Write scope: ${role.writeScope.map((item) => `\`${item}\``).join(", ")}`,
|
|
1072
|
+
`- Outputs: ${role.outputs.length === 0 ? "none" : role.outputs.map((item) => `\`${item}\``).join(", ")}`,
|
|
1073
|
+
`- Evidence required: ${role.evidenceRequired.map((item) => `\`${item}\``).join(", ")}`,
|
|
1074
|
+
`- Forbidden decisions: ${role.forbiddenDecisions.map((item) => `\`${item}\``).join(", ")}`,
|
|
1075
|
+
`- Handoff to: \`${role.handoffTo}\``,
|
|
1076
|
+
`- Stop condition: ${role.stopCondition}`,
|
|
1077
|
+
"",
|
|
1078
|
+
]),
|
|
1079
|
+
].join("\n");
|
|
1080
|
+
}
|
|
1081
|
+
function normalizeStage(value) {
|
|
1082
|
+
return safeSlug(value) || "prepare";
|
|
1083
|
+
}
|
|
1084
|
+
function normalizeList(values) {
|
|
1085
|
+
return unique(values.map(safeSlug));
|
|
1086
|
+
}
|
|
1087
|
+
function safeSlug(value) {
|
|
1088
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1089
|
+
}
|
|
1090
|
+
function unique(values) {
|
|
1091
|
+
return [...new Set(values.filter(isString))];
|
|
1092
|
+
}
|
|
1093
|
+
function isString(value) {
|
|
1094
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
1095
|
+
}
|
|
1096
|
+
function stringValue(value) {
|
|
1097
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
1098
|
+
}
|
|
1099
|
+
function stringList(value) {
|
|
1100
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
1101
|
+
return [value.trim()];
|
|
1102
|
+
if (!Array.isArray(value))
|
|
1103
|
+
return [];
|
|
1104
|
+
return value.filter(isString).map((item) => item.trim());
|
|
1105
|
+
}
|
|
1106
|
+
function booleanValue(value) {
|
|
1107
|
+
return value === true;
|
|
1108
|
+
}
|
|
1109
|
+
function isRecord(value) {
|
|
1110
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1111
|
+
}
|
|
1112
|
+
export function createStaffingRunId(now = new Date()) {
|
|
1113
|
+
return now.toISOString().replace(/[:.]/g, "-");
|
|
1114
|
+
}
|
|
1115
|
+
//# sourceMappingURL=staffing-plan.js.map
|