@captain_z/zsk 1.8.5 → 1.8.7
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 +128 -0
- package/dist/bin.js.map +1 -1
- package/dist/commands/add-flow.js +7 -1
- package/dist/commands/add-flow.js.map +1 -1
- package/dist/commands/add.js +22 -5
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/config.js +3 -2
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/dispatch.d.ts +4 -0
- package/dist/commands/dispatch.js +483 -7
- package/dist/commands/dispatch.js.map +1 -1
- package/dist/commands/doctor.js +21 -5
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/issue.d.ts +1 -0
- package/dist/commands/issue.js +2 -2
- package/dist/commands/issue.js.map +1 -1
- package/dist/commands/prepare.js +7 -0
- package/dist/commands/prepare.js.map +1 -1
- package/dist/commands/project-init.js +15 -8
- package/dist/commands/project-init.js.map +1 -1
- package/dist/commands/work.d.ts +34 -0
- package/dist/commands/work.js +1769 -0
- package/dist/commands/work.js.map +1 -0
- package/dist/commands/workflow.d.ts +32 -0
- package/dist/commands/workflow.js +270 -0
- package/dist/commands/workflow.js.map +1 -0
- package/dist/core/config.d.ts +29 -0
- package/dist/core/config.js +173 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/prepare-lifecycle.d.ts +2 -0
- package/dist/core/prepare-lifecycle.js +49 -0
- package/dist/core/prepare-lifecycle.js.map +1 -1
- package/dist/core/skill-classification.d.ts +13 -0
- package/dist/core/skill-classification.js +50 -0
- package/dist/core/skill-classification.js.map +1 -0
- package/dist/core/stage-clarity-verification.js +58 -7
- package/dist/core/stage-clarity-verification.js.map +1 -1
- package/dist/core/template-registry.js +26 -7
- package/dist/core/template-registry.js.map +1 -1
- package/dist/core/work-ledger.d.ts +44 -0
- package/dist/core/work-ledger.js +88 -0
- package/dist/core/work-ledger.js.map +1 -0
- package/dist/core/work-provider-adapters.d.ts +110 -0
- package/dist/core/work-provider-adapters.js +484 -0
- package/dist/core/work-provider-adapters.js.map +1 -0
- package/dist/core/workflow-graph.d.ts +100 -0
- package/dist/core/workflow-graph.js +655 -0
- package/dist/core/workflow-graph.js.map +1 -0
- package/dist/core/workspace-conformance.js +55 -0
- package/dist/core/workspace-conformance.js.map +1 -1
- package/dist/core/workspace-layout.d.ts +3 -1
- package/dist/core/workspace-layout.js +4 -0
- package/dist/core/workspace-layout.js.map +1 -1
- package/package.json +2 -2
- package/schemas/zsk-config.schema.json +112 -1
- package/templates/module/frontend-module/CONTEXT.md +22 -0
- package/templates/module/frontend-module/design.md +1 -1
- package/templates/module/frontend-module/proposal.md +1 -1
- package/templates/module/frontend-module/spec.md +1 -1
- package/templates/module/frontend-module/tasks.md +14 -1
- package/templates/project-init/.zsk/CONTEXT.md +35 -0
- package/templates/project-init/.zsk/README.md +108 -15
- package/templates/project-init/.zsk/config.yaml +12 -6
- package/templates/project-init/.zsk/docs/CONFIG-SCHEMA.md +21 -5
- package/templates/project-init/.zsk/docs/PROJECT-CONFIG.md +48 -14
- package/templates/project-init/.zsk/docs/SYSTEM-SPEC.md +10 -4
- package/templates/project-init/.zsk/raws/README.md +7 -2
- package/templates/project-init/.zsk/team.yaml +218 -0
- package/templates/project-init/.zsk/templates/config.examples.yaml +29 -8
- package/templates/project-init/.zsk/work.yaml +75 -0
- package/templates/project-init/.zsk/evidence/README.md +0 -15
- package/templates/project-init/.zsk/issues/README.md +0 -10
- package/templates/project-init/.zsk/templates/module/README.md +0 -13
- package/templates/project-init/.zsk/templates/module/design.md +0 -22
- package/templates/project-init/.zsk/templates/module/module.yaml +0 -15
- package/templates/project-init/.zsk/templates/module/proposal.md +0 -20
- package/templates/project-init/.zsk/templates/module/spec.md +0 -22
- package/templates/project-init/.zsk/templates/module/tasks.md +0 -16
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import YAML from "yaml";
|
|
5
|
+
import { templateIdFromConfig } from "./template-registry.js";
|
|
6
|
+
import { getWorkspacePath } from "./workspace-layout.js";
|
|
7
|
+
import { isPathInside, resolveProjectPath } from "./workspace-conformance.js";
|
|
8
|
+
const STAGE_BLUEPRINTS = {
|
|
9
|
+
prepare: {
|
|
10
|
+
id: "prepare",
|
|
11
|
+
skill: "prepare",
|
|
12
|
+
reason: "shared source material should be indexed before downstream stage work starts",
|
|
13
|
+
inputs: () => [".zsk/config.yaml", ".zsk/raws/**", ".zsk/CONTEXT.md"],
|
|
14
|
+
outputs: () => [
|
|
15
|
+
".zsk/raws/manifest.json",
|
|
16
|
+
".zsk/evidence/prepare/{run-id}/acquisition-plan.json",
|
|
17
|
+
".zsk/evidence/prepare/{run-id}/source-promotion-suggestions.md",
|
|
18
|
+
],
|
|
19
|
+
qualityGate: "configured sources are acquired, explicitly blocked, or written as gaps/conflicts",
|
|
20
|
+
capabilities: ["resource-indexing", "source-readiness", "context-record"],
|
|
21
|
+
},
|
|
22
|
+
preproposal: {
|
|
23
|
+
id: "preproposal",
|
|
24
|
+
skill: "preproposal",
|
|
25
|
+
reason: "the intake needs product direction and readiness checks before a formal proposal",
|
|
26
|
+
inputs: (moduleId) => moduleArtifactInputs(moduleId, ["prepare evidence", ".zsk/CONTEXT.md"]),
|
|
27
|
+
outputs: (moduleId) => [modulePath(moduleId, "proposal-readiness.md", ".zsk/docs/proposal-readiness.md")],
|
|
28
|
+
qualityGate: "roadmap, product intent, UX readiness, and source gaps are explicit",
|
|
29
|
+
capabilities: ["product-framing", "readiness-review", "issue-routing"],
|
|
30
|
+
},
|
|
31
|
+
proposal: {
|
|
32
|
+
id: "proposal",
|
|
33
|
+
skill: "proposal",
|
|
34
|
+
reason: "scope and success criteria need to be frozen before detailed requirements",
|
|
35
|
+
inputs: (moduleId) => moduleArtifactInputs(moduleId, ["prepare evidence", ".zsk/CONTEXT.md"]),
|
|
36
|
+
outputs: (moduleId) => [modulePath(moduleId, "proposal.md", ".zsk/docs/proposal.md")],
|
|
37
|
+
qualityGate: "problem, non-goals, success criteria, stakeholders, and open questions are sourced",
|
|
38
|
+
capabilities: ["issue-routing", "source-traceability"],
|
|
39
|
+
},
|
|
40
|
+
spec: {
|
|
41
|
+
id: "spec",
|
|
42
|
+
skill: "spec",
|
|
43
|
+
reason: "requirements and acceptance criteria must become testable downstream inputs",
|
|
44
|
+
inputs: (moduleId) => [modulePath(moduleId, "proposal.md", ".zsk/docs/proposal.md"), ".zsk/CONTEXT.md"],
|
|
45
|
+
outputs: (moduleId) => [modulePath(moduleId, "spec.md", ".zsk/docs/spec.md")],
|
|
46
|
+
qualityGate: "FR/NFR, AC, scenarios, edge cases, and unresolved questions are traceable",
|
|
47
|
+
capabilities: ["acceptance-criteria", "source-traceability", "issue-routing"],
|
|
48
|
+
},
|
|
49
|
+
design: {
|
|
50
|
+
id: "design",
|
|
51
|
+
skill: "design",
|
|
52
|
+
reason: "interfaces, data flow, state, rollout, and risks should be explicit before tasks",
|
|
53
|
+
inputs: (moduleId) => [modulePath(moduleId, "spec.md", ".zsk/docs/spec.md"), ".zsk/CONTEXT.md"],
|
|
54
|
+
outputs: (moduleId) => [modulePath(moduleId, "design.md", ".zsk/docs/design.md")],
|
|
55
|
+
qualityGate: "implementation surfaces, ADRs, triggered controls, and risks are inspectable",
|
|
56
|
+
capabilities: ["architecture", "ux-flow", "risk-review"],
|
|
57
|
+
},
|
|
58
|
+
task: {
|
|
59
|
+
id: "task",
|
|
60
|
+
skill: "task",
|
|
61
|
+
reason: "the implementation should be split into issue-sized tasks before coding starts",
|
|
62
|
+
inputs: (moduleId) => [
|
|
63
|
+
modulePath(moduleId, "spec.md", ".zsk/docs/spec.md"),
|
|
64
|
+
modulePath(moduleId, "design.md", ".zsk/docs/design.md"),
|
|
65
|
+
".zsk/work.yaml",
|
|
66
|
+
".zsk/team.yaml",
|
|
67
|
+
],
|
|
68
|
+
outputs: (moduleId) => [
|
|
69
|
+
modulePath(moduleId, "tasks.md", ".zsk/docs/tasks.md"),
|
|
70
|
+
".zsk/work.jsonl",
|
|
71
|
+
],
|
|
72
|
+
qualityGate: "tasks map to FR/AC, dependencies, owners, evidence hooks, and external work ids where available",
|
|
73
|
+
capabilities: ["issue-decomposition", "work-ledger", "talent-routing"],
|
|
74
|
+
},
|
|
75
|
+
coding: {
|
|
76
|
+
id: "coding",
|
|
77
|
+
skill: "coding",
|
|
78
|
+
reason: "the scoped task can now be implemented with fresh local evidence",
|
|
79
|
+
inputs: (moduleId) => [modulePath(moduleId, "tasks.md", ".zsk/docs/tasks.md"), ".zsk/work.jsonl"],
|
|
80
|
+
outputs: (moduleId) => [modulePath(moduleId, "_evidence/coding", ".zsk/evidence/coding")],
|
|
81
|
+
qualityGate: "changed behavior has targeted tests, lint/type evidence, and no unresolved blocker",
|
|
82
|
+
capabilities: ["tdd", "smoke", "work-status"],
|
|
83
|
+
},
|
|
84
|
+
demo: {
|
|
85
|
+
id: "demo",
|
|
86
|
+
skill: "demo",
|
|
87
|
+
reason: "browser-facing behavior should be demonstrated before formal verification",
|
|
88
|
+
inputs: (moduleId) => [
|
|
89
|
+
modulePath(moduleId, "_playwright/specs", ".zsk/playwright/specs"),
|
|
90
|
+
modulePath(moduleId, "_playwright/test-plans", ".zsk/playwright/test-plans"),
|
|
91
|
+
],
|
|
92
|
+
outputs: (moduleId) => [modulePath(moduleId, "_evidence/demo", ".zsk/evidence/demo")],
|
|
93
|
+
qualityGate: "demo evidence includes reproducible screenshots/video/trace/logs or an explicit no-demo rationale",
|
|
94
|
+
capabilities: ["playwright", "visual-evidence", "issue-routing"],
|
|
95
|
+
},
|
|
96
|
+
verify: {
|
|
97
|
+
id: "verify",
|
|
98
|
+
skill: "verify",
|
|
99
|
+
reason: "the completion claim must be checked against acceptance criteria and evidence",
|
|
100
|
+
inputs: (moduleId) => [
|
|
101
|
+
modulePath(moduleId, "spec.md", ".zsk/docs/spec.md"),
|
|
102
|
+
modulePath(moduleId, "_evidence", ".zsk/evidence"),
|
|
103
|
+
],
|
|
104
|
+
outputs: (moduleId) => [modulePath(moduleId, "verify.md", ".zsk/docs/verify.md")],
|
|
105
|
+
qualityGate: "fresh evidence proves or blocks every claimed acceptance criterion",
|
|
106
|
+
capabilities: ["verification", "evidence-review", "issue-routing"],
|
|
107
|
+
},
|
|
108
|
+
acceptance: {
|
|
109
|
+
id: "acceptance",
|
|
110
|
+
skill: "acceptance",
|
|
111
|
+
reason: "stakeholder acceptance or explicit rejection should be recorded before closure",
|
|
112
|
+
inputs: (moduleId) => [modulePath(moduleId, "verify.md", ".zsk/docs/verify.md")],
|
|
113
|
+
outputs: (moduleId) => [modulePath(moduleId, "acceptance.md", ".zsk/docs/acceptance.md")],
|
|
114
|
+
qualityGate: "accept/reject decision links evidence and residual risks",
|
|
115
|
+
capabilities: ["decision-record", "risk-ownership"],
|
|
116
|
+
},
|
|
117
|
+
archive: {
|
|
118
|
+
id: "archive",
|
|
119
|
+
skill: "archive",
|
|
120
|
+
reason: "durable decisions, evidence, and learning should be preserved after acceptance",
|
|
121
|
+
inputs: (moduleId) => [modulePath(moduleId, "acceptance.md", ".zsk/docs/acceptance.md")],
|
|
122
|
+
outputs: (moduleId) => [modulePath(moduleId, "archive.md", ".zsk/docs/archive.md")],
|
|
123
|
+
qualityGate: "handoff, evidence, issues, decisions, and documentation feedback are closed or linked",
|
|
124
|
+
capabilities: ["archive", "documentation-feedback", "learning"],
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
const PROFILE_FLOWS = {
|
|
128
|
+
"zsk-entry": ["coding"],
|
|
129
|
+
"zsk-lite": ["coding"],
|
|
130
|
+
"zsk-sdlc": ["prepare", "proposal", "spec", "design", "task", "coding", "verify", "acceptance", "archive"],
|
|
131
|
+
"zsk-frontend": ["prepare", "proposal", "spec", "design", "task", "coding", "demo", "verify", "acceptance", "archive"],
|
|
132
|
+
"zsk-enterprise": ["prepare", "proposal", "spec", "design", "task", "coding", "verify", "acceptance", "archive"],
|
|
133
|
+
};
|
|
134
|
+
export async function buildWorkflowGraphBundle(target, config, opts = {}) {
|
|
135
|
+
const workflowPolicy = await readWorkflowPolicy(target, config);
|
|
136
|
+
const profile = opts.profile ?? workflowPolicy.profile ?? config.workflow?.profile ?? "zsk-sdlc";
|
|
137
|
+
const runId = opts.runId ?? createWorkflowRunId();
|
|
138
|
+
const now = new Date().toISOString();
|
|
139
|
+
const explicitStages = parseStages(opts.stages);
|
|
140
|
+
const selection = selectStages(config, workflowPolicy, profile, opts.goal, explicitStages);
|
|
141
|
+
const stages = buildStages(selection.stages, opts.module, selection.failureReturnsTo);
|
|
142
|
+
const artifacts = resolveWorkflowArtifacts(target, config, runId);
|
|
143
|
+
const graph = {
|
|
144
|
+
version: 1,
|
|
145
|
+
runId,
|
|
146
|
+
createdAt: now,
|
|
147
|
+
updatedAt: now,
|
|
148
|
+
goal: normalizeGoal(opts.goal),
|
|
149
|
+
project: {
|
|
150
|
+
name: config.project?.name ?? config.project?.display_name ?? "<project-name>",
|
|
151
|
+
...(opts.module ? { module: opts.module } : {}),
|
|
152
|
+
template: templateIdFromConfig(config),
|
|
153
|
+
},
|
|
154
|
+
profile,
|
|
155
|
+
strategy: workflowPolicy.strategy ?? config.workflow?.strategy ?? "adaptive",
|
|
156
|
+
selection: {
|
|
157
|
+
source: explicitStages.length > 0 ? "explicit" : "auto",
|
|
158
|
+
preferMinimalStages: selection.preferMinimalStages,
|
|
159
|
+
preferEvidenceStages: selection.preferEvidenceStages,
|
|
160
|
+
allowCustomStages: selection.allowCustomStages,
|
|
161
|
+
requireStageContract: selection.requireStageContract,
|
|
162
|
+
reasons: selection.reasons,
|
|
163
|
+
},
|
|
164
|
+
stages,
|
|
165
|
+
stopCondition: "all required stages are passed, skipped with rationale, or blocked with an issue and handoff",
|
|
166
|
+
artifacts: {
|
|
167
|
+
yaml: projectRelative(target, artifacts.yamlPath),
|
|
168
|
+
json: projectRelative(target, artifacts.jsonPath),
|
|
169
|
+
markdown: projectRelative(target, artifacts.markdownPath),
|
|
170
|
+
current: projectRelative(target, artifacts.currentPath),
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
graph,
|
|
175
|
+
markdown: renderWorkflowGraphMarkdown(graph),
|
|
176
|
+
artifacts,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export async function writeWorkflowGraphBundle(bundle) {
|
|
180
|
+
await mkdir(bundle.artifacts.dir, { recursive: true });
|
|
181
|
+
await mkdir(dirname(bundle.artifacts.currentPath), { recursive: true });
|
|
182
|
+
await writeFile(bundle.artifacts.jsonPath, `${JSON.stringify(bundle.graph, null, 2)}\n`, "utf8");
|
|
183
|
+
await writeFile(bundle.artifacts.yamlPath, YAML.stringify(bundle.graph), "utf8");
|
|
184
|
+
await writeFile(bundle.artifacts.markdownPath, bundle.markdown, "utf8");
|
|
185
|
+
await writeFile(bundle.artifacts.currentPath, `${JSON.stringify({
|
|
186
|
+
runId: bundle.graph.runId,
|
|
187
|
+
graph: bundle.graph.artifacts.json,
|
|
188
|
+
markdown: bundle.graph.artifacts.markdown,
|
|
189
|
+
yaml: bundle.graph.artifacts.yaml,
|
|
190
|
+
updatedAt: bundle.graph.updatedAt,
|
|
191
|
+
}, null, 2)}\n`, "utf8");
|
|
192
|
+
}
|
|
193
|
+
export async function loadWorkflowGraph(target, config, runId) {
|
|
194
|
+
const plansRoot = resolve(target, getWorkspacePath(config, "plansRoot"));
|
|
195
|
+
const graphPath = runId
|
|
196
|
+
? join(plansRoot, runId, "workflow-graph.json")
|
|
197
|
+
: await currentWorkflowGraphPath(target, config);
|
|
198
|
+
const content = await readFile(graphPath, "utf8");
|
|
199
|
+
const parsed = JSON.parse(content);
|
|
200
|
+
return { graph: parsed, path: graphPath };
|
|
201
|
+
}
|
|
202
|
+
export async function markWorkflowStage(target, config, opts) {
|
|
203
|
+
const { graph } = await loadWorkflowGraph(target, config, opts.runId);
|
|
204
|
+
const stage = graph.stages.find((candidate) => candidate.id === normalizeStageId(opts.stage));
|
|
205
|
+
if (!stage) {
|
|
206
|
+
throw new Error(`Unknown workflow stage: ${opts.stage}`);
|
|
207
|
+
}
|
|
208
|
+
const updatedAt = new Date().toISOString();
|
|
209
|
+
stage.status = opts.status;
|
|
210
|
+
stage.updatedAt = updatedAt;
|
|
211
|
+
if (opts.note)
|
|
212
|
+
stage.note = opts.note;
|
|
213
|
+
graph.updatedAt = updatedAt;
|
|
214
|
+
const artifacts = resolveWorkflowArtifacts(target, config, graph.runId);
|
|
215
|
+
return {
|
|
216
|
+
graph,
|
|
217
|
+
markdown: renderWorkflowGraphMarkdown(graph),
|
|
218
|
+
artifacts,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export async function advanceWorkflowGraph(target, config, opts = {}) {
|
|
222
|
+
const { graph } = await loadWorkflowGraph(target, config, opts.runId);
|
|
223
|
+
const stage = opts.stage
|
|
224
|
+
? graph.stages.find((candidate) => candidate.id === normalizeStageId(opts.stage))
|
|
225
|
+
: nextWorkflowStage(graph);
|
|
226
|
+
const artifacts = resolveWorkflowArtifacts(target, config, graph.runId);
|
|
227
|
+
if (!stage) {
|
|
228
|
+
return {
|
|
229
|
+
graph,
|
|
230
|
+
markdown: renderWorkflowGraphMarkdown(graph),
|
|
231
|
+
artifacts,
|
|
232
|
+
updated: false,
|
|
233
|
+
result: {
|
|
234
|
+
runId: graph.runId,
|
|
235
|
+
action: "unchanged",
|
|
236
|
+
reason: "no pending workflow stage found",
|
|
237
|
+
evidence: [],
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const previousStatus = stage.status;
|
|
242
|
+
const decision = await decideWorkflowAdvance(target, config, graph, stage);
|
|
243
|
+
if (decision.action !== "unchanged") {
|
|
244
|
+
const updatedAt = new Date().toISOString();
|
|
245
|
+
stage.status = decision.action === "passed" ? "passed" : "blocked";
|
|
246
|
+
stage.updatedAt = updatedAt;
|
|
247
|
+
stage.note = decision.reason;
|
|
248
|
+
graph.updatedAt = updatedAt;
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
graph,
|
|
252
|
+
markdown: renderWorkflowGraphMarkdown(graph),
|
|
253
|
+
artifacts,
|
|
254
|
+
updated: decision.action !== "unchanged",
|
|
255
|
+
result: {
|
|
256
|
+
runId: graph.runId,
|
|
257
|
+
stage: stage.id,
|
|
258
|
+
previousStatus,
|
|
259
|
+
status: stage.status,
|
|
260
|
+
action: decision.action,
|
|
261
|
+
reason: decision.reason,
|
|
262
|
+
evidence: decision.evidence,
|
|
263
|
+
next: nextWorkflowStage(graph),
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
export function nextWorkflowStage(graph) {
|
|
268
|
+
return graph.stages.find((stage) => stage.status === "pending" || stage.status === "running" || stage.status === "blocked");
|
|
269
|
+
}
|
|
270
|
+
export function renderWorkflowGraphMarkdown(graph) {
|
|
271
|
+
return [
|
|
272
|
+
`# Workflow Graph: ${graph.goal}`,
|
|
273
|
+
"",
|
|
274
|
+
`- run_id: ${graph.runId}`,
|
|
275
|
+
`- profile: ${graph.profile}`,
|
|
276
|
+
`- strategy: ${graph.strategy}`,
|
|
277
|
+
`- module: ${graph.project.module ?? "project"}`,
|
|
278
|
+
`- selection: ${graph.selection.source}`,
|
|
279
|
+
`- stop_condition: ${graph.stopCondition}`,
|
|
280
|
+
"",
|
|
281
|
+
"## Selection Reasons",
|
|
282
|
+
"",
|
|
283
|
+
...graph.selection.reasons.map((reason) => `- ${reason}`),
|
|
284
|
+
"",
|
|
285
|
+
"## Stages",
|
|
286
|
+
"",
|
|
287
|
+
...graph.stages.flatMap((stage, index) => [
|
|
288
|
+
`### ${index + 1}. ${stage.id}`,
|
|
289
|
+
"",
|
|
290
|
+
`- status: ${stage.status}`,
|
|
291
|
+
`- skill: ${stage.skill}`,
|
|
292
|
+
`- reason: ${stage.reason}`,
|
|
293
|
+
`- depends_on: ${stage.dependsOn.length ? stage.dependsOn.join(", ") : "none"}`,
|
|
294
|
+
`- quality_gate: ${stage.qualityGate}`,
|
|
295
|
+
`- failure_returns_to: ${stage.failureReturnsTo}`,
|
|
296
|
+
`- capabilities: ${stage.capabilities.join(", ")}`,
|
|
297
|
+
"- inputs:",
|
|
298
|
+
...stage.inputs.map((input) => ` - ${input}`),
|
|
299
|
+
"- outputs:",
|
|
300
|
+
...stage.outputs.map((output) => ` - ${output}`),
|
|
301
|
+
"- command_hints:",
|
|
302
|
+
...stage.commandHints.map((command) => ` - ${command}`),
|
|
303
|
+
...(stage.note ? [`- note: ${stage.note}`] : []),
|
|
304
|
+
"",
|
|
305
|
+
]),
|
|
306
|
+
].join("\n");
|
|
307
|
+
}
|
|
308
|
+
function selectStages(config, workflowPolicy, profile, goal, explicitStages) {
|
|
309
|
+
const selection = {
|
|
310
|
+
...(config.workflow?.stageSelection ?? {}),
|
|
311
|
+
...(workflowPolicy.stageSelection ?? {}),
|
|
312
|
+
};
|
|
313
|
+
const allowCustomStages = selection.allowCustomStages ?? true;
|
|
314
|
+
const requireStageContract = selection.requireStageContract ?? true;
|
|
315
|
+
const preferMinimalStages = selection.preferMinimalStages ?? (profile === "zsk-entry" || profile === "zsk-lite");
|
|
316
|
+
const preferEvidenceStages = selection.preferEvidenceStages ?? (profile === "zsk-frontend" || profile === "zsk-enterprise");
|
|
317
|
+
const failureReturnsTo = workflowPolicy.workflowGraph?.failureReturnsTo ?? config.workflow?.workflowGraph?.failureReturnsTo ?? "nearest-owner-stage";
|
|
318
|
+
if (explicitStages.length > 0) {
|
|
319
|
+
return {
|
|
320
|
+
stages: normalizeStageList(explicitStages, allowCustomStages),
|
|
321
|
+
preferMinimalStages,
|
|
322
|
+
preferEvidenceStages,
|
|
323
|
+
allowCustomStages,
|
|
324
|
+
requireStageContract,
|
|
325
|
+
failureReturnsTo,
|
|
326
|
+
reasons: [
|
|
327
|
+
"explicit stage list was supplied by the caller",
|
|
328
|
+
`custom stages are ${allowCustomStages ? "allowed" : "rejected unless known"}`,
|
|
329
|
+
],
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const goalText = (goal ?? "").toLowerCase();
|
|
333
|
+
const reasons = [`profile ${profile} selected ${profileRoute(profile)}`];
|
|
334
|
+
let stages = [...(PROFILE_FLOWS[profile] ?? PROFILE_FLOWS["zsk-sdlc"] ?? [])];
|
|
335
|
+
if (preferMinimalStages) {
|
|
336
|
+
stages = hasConfiguredResources(config) ? ["prepare", "coding", "verify"] : ["coding", "verify"];
|
|
337
|
+
reasons.push("minimal stage preference reduced the graph to implementation plus verification");
|
|
338
|
+
}
|
|
339
|
+
if (!preferMinimalStages && shouldInsertPreproposal(goalText)) {
|
|
340
|
+
stages = insertAfter(stages, "prepare", "preproposal");
|
|
341
|
+
reasons.push("brief appears underspecified, so preproposal readiness was inserted");
|
|
342
|
+
}
|
|
343
|
+
if (shouldInsertDemo(goalText, profile, preferEvidenceStages)) {
|
|
344
|
+
stages = insertBefore(stages, "verify", "demo");
|
|
345
|
+
reasons.push("browser, UI, visual, or evidence-sensitive work requires a demo stage");
|
|
346
|
+
}
|
|
347
|
+
if (profile === "zsk-enterprise") {
|
|
348
|
+
stages = appendMissing(stages, ["acceptance", "archive"]);
|
|
349
|
+
reasons.push("enterprise profile keeps acceptance and archive gates explicit");
|
|
350
|
+
}
|
|
351
|
+
stages = normalizeStageList(stages, allowCustomStages);
|
|
352
|
+
return {
|
|
353
|
+
stages,
|
|
354
|
+
preferMinimalStages,
|
|
355
|
+
preferEvidenceStages,
|
|
356
|
+
allowCustomStages,
|
|
357
|
+
requireStageContract,
|
|
358
|
+
failureReturnsTo,
|
|
359
|
+
reasons,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function buildStages(stageIds, moduleId, failureReturnsTo) {
|
|
363
|
+
return stageIds.map((id, index) => {
|
|
364
|
+
const previous = stageIds[index - 1];
|
|
365
|
+
const blueprint = STAGE_BLUEPRINTS[id] ?? genericStageBlueprint(id);
|
|
366
|
+
const skill = blueprint.skill;
|
|
367
|
+
const stage = {
|
|
368
|
+
id,
|
|
369
|
+
skill,
|
|
370
|
+
status: "pending",
|
|
371
|
+
reason: blueprint.reason,
|
|
372
|
+
dependsOn: previous ? [previous] : [],
|
|
373
|
+
inputs: blueprint.inputs(moduleId),
|
|
374
|
+
outputs: blueprint.outputs(moduleId),
|
|
375
|
+
qualityGate: blueprint.qualityGate,
|
|
376
|
+
failureReturnsTo,
|
|
377
|
+
capabilities: blueprint.capabilities,
|
|
378
|
+
commandHints: commandHints(id, skill, moduleId),
|
|
379
|
+
};
|
|
380
|
+
return stage;
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
async function readWorkflowPolicy(target, config) {
|
|
384
|
+
const policyPath = config.customize?.workflow;
|
|
385
|
+
if (!policyPath || typeof policyPath !== "string")
|
|
386
|
+
return {};
|
|
387
|
+
const resolved = resolveProjectPath(target, policyPath);
|
|
388
|
+
if (!isPathInside(target, resolved) || !(await exists(resolved)))
|
|
389
|
+
return {};
|
|
390
|
+
const parsed = YAML.parse(await readFile(resolved, "utf8"));
|
|
391
|
+
if (!isRecord(parsed))
|
|
392
|
+
return {};
|
|
393
|
+
const policy = parsed;
|
|
394
|
+
return {
|
|
395
|
+
...(typeof policy.profile === "string" ? { profile: policy.profile } : {}),
|
|
396
|
+
...(typeof policy.strategy === "string" ? { strategy: policy.strategy } : {}),
|
|
397
|
+
...(isRecord(policy.stageSelection) ? { stageSelection: policy.stageSelection } : {}),
|
|
398
|
+
...(isRecord(policy.workflowGraph) ? { workflowGraph: policy.workflowGraph } : {}),
|
|
399
|
+
...(isRecord(policy.capabilities) ? { capabilities: policy.capabilities } : {}),
|
|
400
|
+
...(isRecord(policy.qualityGates) ? { qualityGates: policy.qualityGates } : {}),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function resolveWorkflowArtifacts(target, config, runId) {
|
|
404
|
+
const plansRoot = resolve(target, getWorkspacePath(config, "plansRoot"));
|
|
405
|
+
const dir = join(plansRoot, runId);
|
|
406
|
+
return {
|
|
407
|
+
dir,
|
|
408
|
+
yamlPath: join(dir, "workflow-graph.yaml"),
|
|
409
|
+
jsonPath: join(dir, "workflow-graph.json"),
|
|
410
|
+
markdownPath: join(dir, "workflow-graph.md"),
|
|
411
|
+
currentPath: join(plansRoot, "current-workflow-graph.json"),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
async function decideWorkflowAdvance(target, config, graph, stage) {
|
|
415
|
+
const gate = await latestGateAssessment(target, config, stage.id, graph.project.module);
|
|
416
|
+
if (gate) {
|
|
417
|
+
const evidence = [projectRelative(target, gate.path)];
|
|
418
|
+
if ((gate.assessment.status === "READY" || gate.assessment.status === "WAIVED") && gate.assessment.decision === "proceed") {
|
|
419
|
+
return {
|
|
420
|
+
action: "passed",
|
|
421
|
+
reason: `stage gate ${gate.assessment.status} allows proceed`,
|
|
422
|
+
evidence,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
if (gate.assessment.status === "BLOCKED" || gate.assessment.decision === "blocked") {
|
|
426
|
+
return {
|
|
427
|
+
action: "blocked",
|
|
428
|
+
reason: `stage gate blocked: ${firstReason(gate.assessment.blockers)}`,
|
|
429
|
+
evidence,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
if (gate.assessment.status === "NEEDS_CONFIRMATION") {
|
|
433
|
+
return {
|
|
434
|
+
action: "blocked",
|
|
435
|
+
reason: `stage gate needs confirmation: ${firstReason(gate.assessment.gaps)}`,
|
|
436
|
+
evidence,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (stage.id === "prepare") {
|
|
441
|
+
const manifestPath = resolve(target, getWorkspacePath(config, "rawsManifest"));
|
|
442
|
+
if (await exists(manifestPath)) {
|
|
443
|
+
return {
|
|
444
|
+
action: "passed",
|
|
445
|
+
reason: "prepare raw manifest exists",
|
|
446
|
+
evidence: [projectRelative(target, manifestPath)],
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
action: "unchanged",
|
|
452
|
+
reason: `no decisive evidence found for ${stage.id}`,
|
|
453
|
+
evidence: [],
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
async function latestGateAssessment(target, config, stage, moduleId) {
|
|
457
|
+
const gatesRoot = resolve(target, getWorkspacePath(config, "evidenceRoot"), "gates");
|
|
458
|
+
let entries;
|
|
459
|
+
try {
|
|
460
|
+
entries = (await readdir(gatesRoot, { withFileTypes: true }))
|
|
461
|
+
.filter((entry) => entry.isDirectory())
|
|
462
|
+
.map((entry) => entry.name)
|
|
463
|
+
.sort()
|
|
464
|
+
.reverse();
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
for (const entry of entries) {
|
|
470
|
+
const file = join(gatesRoot, entry, "summary.json");
|
|
471
|
+
if (!(await exists(file)))
|
|
472
|
+
continue;
|
|
473
|
+
try {
|
|
474
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
475
|
+
if (!isRecord(parsed))
|
|
476
|
+
continue;
|
|
477
|
+
if (parsed.stage !== stage)
|
|
478
|
+
continue;
|
|
479
|
+
if (moduleId) {
|
|
480
|
+
if (parsed.module !== moduleId)
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
else if (typeof parsed.module === "string" && parsed.module.trim().length > 0) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
path: file,
|
|
488
|
+
assessment: {
|
|
489
|
+
stage: typeof parsed.stage === "string" ? parsed.stage : undefined,
|
|
490
|
+
module: typeof parsed.module === "string" ? parsed.module : undefined,
|
|
491
|
+
status: typeof parsed.status === "string" ? parsed.status : undefined,
|
|
492
|
+
decision: typeof parsed.decision === "string" ? parsed.decision : undefined,
|
|
493
|
+
blockers: Array.isArray(parsed.blockers) ? parsed.blockers.filter((item) => typeof item === "string") : undefined,
|
|
494
|
+
gaps: Array.isArray(parsed.gaps) ? parsed.gaps.filter((item) => typeof item === "string") : undefined,
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return undefined;
|
|
503
|
+
}
|
|
504
|
+
async function currentWorkflowGraphPath(target, config) {
|
|
505
|
+
const plansRoot = resolve(target, getWorkspacePath(config, "plansRoot"));
|
|
506
|
+
const currentPath = join(plansRoot, "current-workflow-graph.json");
|
|
507
|
+
if (await exists(currentPath)) {
|
|
508
|
+
const parsed = JSON.parse(await readFile(currentPath, "utf8"));
|
|
509
|
+
if (typeof parsed.graph === "string" && parsed.graph.trim()) {
|
|
510
|
+
const graphPath = resolveProjectPath(target, parsed.graph);
|
|
511
|
+
if (!isPathInside(target, graphPath))
|
|
512
|
+
throw new Error("Current workflow graph pointer escapes the project.");
|
|
513
|
+
return graphPath;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const entries = await readdir(plansRoot, { withFileTypes: true });
|
|
517
|
+
const latest = entries
|
|
518
|
+
.filter((entry) => entry.isDirectory())
|
|
519
|
+
.map((entry) => entry.name)
|
|
520
|
+
.sort()
|
|
521
|
+
.at(-1);
|
|
522
|
+
if (!latest)
|
|
523
|
+
throw new Error("No workflow graph found; run `zsk workflow plan` first.");
|
|
524
|
+
return join(plansRoot, latest, "workflow-graph.json");
|
|
525
|
+
}
|
|
526
|
+
function commandHints(stage, skill, moduleId) {
|
|
527
|
+
const moduleFlag = moduleId ? ` -m ${moduleId}` : "";
|
|
528
|
+
if (stage === "prepare") {
|
|
529
|
+
return ["zsk prepare run"];
|
|
530
|
+
}
|
|
531
|
+
if (stage === "coding") {
|
|
532
|
+
return [
|
|
533
|
+
`zsk gate assess --stage coding${moduleFlag}`,
|
|
534
|
+
`zsk dispatch plan --stage coding --skill ${skill}${moduleFlag} --surface auto`,
|
|
535
|
+
];
|
|
536
|
+
}
|
|
537
|
+
return [
|
|
538
|
+
`zsk gate assess --stage ${stage}${moduleFlag}`,
|
|
539
|
+
`zsk dispatch plan --stage ${stage} --skill ${skill}${moduleFlag} --surface auto`,
|
|
540
|
+
];
|
|
541
|
+
}
|
|
542
|
+
function moduleArtifactInputs(moduleId, fallback) {
|
|
543
|
+
if (!moduleId)
|
|
544
|
+
return fallback;
|
|
545
|
+
return [".zsk/raws/manifest.json", `.zsk/modules/${moduleId}/CONTEXT.md`, ...fallback];
|
|
546
|
+
}
|
|
547
|
+
function modulePath(moduleId, file, fallback) {
|
|
548
|
+
return moduleId ? join(".zsk/modules", moduleId, file) : fallback;
|
|
549
|
+
}
|
|
550
|
+
function normalizeGoal(goal) {
|
|
551
|
+
const trimmed = goal?.trim();
|
|
552
|
+
return trimmed && trimmed.length > 0 ? trimmed : "Continue the current ZSK delivery workflow";
|
|
553
|
+
}
|
|
554
|
+
function parseStages(value) {
|
|
555
|
+
if (!value)
|
|
556
|
+
return [];
|
|
557
|
+
return value.split(",").map((stage) => normalizeStageId(stage)).filter(Boolean);
|
|
558
|
+
}
|
|
559
|
+
function normalizeStageList(stages, allowCustomStages) {
|
|
560
|
+
const normalized = [];
|
|
561
|
+
for (const stage of stages) {
|
|
562
|
+
const id = normalizeStageId(stage);
|
|
563
|
+
if (!id || normalized.includes(id))
|
|
564
|
+
continue;
|
|
565
|
+
if (!allowCustomStages && !STAGE_BLUEPRINTS[id])
|
|
566
|
+
continue;
|
|
567
|
+
normalized.push(id);
|
|
568
|
+
}
|
|
569
|
+
return normalized;
|
|
570
|
+
}
|
|
571
|
+
function normalizeStageId(value) {
|
|
572
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
573
|
+
}
|
|
574
|
+
function genericStageBlueprint(id) {
|
|
575
|
+
return {
|
|
576
|
+
id,
|
|
577
|
+
skill: id,
|
|
578
|
+
reason: "custom stage selected by workflow graph policy",
|
|
579
|
+
inputs: () => [".zsk/CONTEXT.md", ".zsk/plans/current-workflow-graph.json"],
|
|
580
|
+
outputs: () => [join(".zsk/evidence", id)],
|
|
581
|
+
qualityGate: "stage contract, sourced output, and explicit handoff are present",
|
|
582
|
+
capabilities: ["custom-stage", "issue-routing", "evidence"],
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function hasConfiguredResources(config) {
|
|
586
|
+
return Object.keys(config.sources ?? {}).length > 0 || (config.resources ?? []).length > 0;
|
|
587
|
+
}
|
|
588
|
+
function shouldInsertPreproposal(goalText) {
|
|
589
|
+
if (!goalText)
|
|
590
|
+
return true;
|
|
591
|
+
if (/(vague|unclear|idea|brainstorm|探索|想法|模糊|不清楚|不确定)/i.test(goalText))
|
|
592
|
+
return true;
|
|
593
|
+
const tokens = goalText.split(/\s+/).filter(Boolean);
|
|
594
|
+
return tokens.length > 0 && tokens.length <= 4;
|
|
595
|
+
}
|
|
596
|
+
function shouldInsertDemo(_goalText, profile, _preferEvidenceStages) {
|
|
597
|
+
return profile === "zsk-frontend";
|
|
598
|
+
}
|
|
599
|
+
function insertAfter(stages, anchor, stage) {
|
|
600
|
+
if (stages.includes(stage))
|
|
601
|
+
return stages;
|
|
602
|
+
const index = stages.indexOf(anchor);
|
|
603
|
+
if (index === -1)
|
|
604
|
+
return [stage, ...stages];
|
|
605
|
+
return [...stages.slice(0, index + 1), stage, ...stages.slice(index + 1)];
|
|
606
|
+
}
|
|
607
|
+
function insertBefore(stages, anchor, stage) {
|
|
608
|
+
if (stages.includes(stage))
|
|
609
|
+
return stages;
|
|
610
|
+
const index = stages.indexOf(anchor);
|
|
611
|
+
if (index === -1)
|
|
612
|
+
return [...stages, stage];
|
|
613
|
+
return [...stages.slice(0, index), stage, ...stages.slice(index)];
|
|
614
|
+
}
|
|
615
|
+
function appendMissing(stages, additions) {
|
|
616
|
+
const next = [...stages];
|
|
617
|
+
for (const stage of additions) {
|
|
618
|
+
if (!next.includes(stage))
|
|
619
|
+
next.push(stage);
|
|
620
|
+
}
|
|
621
|
+
return next;
|
|
622
|
+
}
|
|
623
|
+
function firstReason(items) {
|
|
624
|
+
return items?.find((item) => item.trim().length > 0) ?? "see gate assessment";
|
|
625
|
+
}
|
|
626
|
+
function profileRoute(profile) {
|
|
627
|
+
return profile === "zsk-lite" || profile === "zsk-entry"
|
|
628
|
+
? "adaptive-minimal"
|
|
629
|
+
: profile === "zsk-frontend"
|
|
630
|
+
? "adaptive-frontend"
|
|
631
|
+
: profile === "zsk-enterprise"
|
|
632
|
+
? "adaptive-enterprise"
|
|
633
|
+
: "adaptive-sdlc";
|
|
634
|
+
}
|
|
635
|
+
function projectRelative(root, value) {
|
|
636
|
+
const resolved = isAbsolute(value) ? value : resolve(root, value);
|
|
637
|
+
const rel = relative(root, resolved);
|
|
638
|
+
return rel === "" ? basename(resolved) : rel;
|
|
639
|
+
}
|
|
640
|
+
function createWorkflowRunId(now = new Date()) {
|
|
641
|
+
return `${now.toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 12)}`;
|
|
642
|
+
}
|
|
643
|
+
async function exists(path) {
|
|
644
|
+
try {
|
|
645
|
+
await access(path);
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function isRecord(value) {
|
|
653
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
654
|
+
}
|
|
655
|
+
//# sourceMappingURL=workflow-graph.js.map
|