@captain_z/zsk 1.8.6 → 1.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +34 -16
  2. package/dist/bin.js +134 -0
  3. package/dist/bin.js.map +1 -1
  4. package/dist/commands/add-flow.js +7 -1
  5. package/dist/commands/add-flow.js.map +1 -1
  6. package/dist/commands/add.js +22 -5
  7. package/dist/commands/add.js.map +1 -1
  8. package/dist/commands/dispatch.d.ts +4 -0
  9. package/dist/commands/dispatch.js +483 -7
  10. package/dist/commands/dispatch.js.map +1 -1
  11. package/dist/commands/doctor.js +21 -5
  12. package/dist/commands/doctor.js.map +1 -1
  13. package/dist/commands/issue.d.ts +1 -0
  14. package/dist/commands/issue.js +2 -2
  15. package/dist/commands/issue.js.map +1 -1
  16. package/dist/commands/prepare.js +3 -0
  17. package/dist/commands/prepare.js.map +1 -1
  18. package/dist/commands/project-init.js +14 -8
  19. package/dist/commands/project-init.js.map +1 -1
  20. package/dist/commands/work.d.ts +34 -0
  21. package/dist/commands/work.js +1769 -0
  22. package/dist/commands/work.js.map +1 -0
  23. package/dist/commands/workflow.d.ts +32 -0
  24. package/dist/commands/workflow.js +270 -0
  25. package/dist/commands/workflow.js.map +1 -0
  26. package/dist/core/anchor-drift.d.ts +79 -0
  27. package/dist/core/anchor-drift.js +198 -0
  28. package/dist/core/anchor-drift.js.map +1 -0
  29. package/dist/core/config.d.ts +29 -0
  30. package/dist/core/config.js +157 -1
  31. package/dist/core/config.js.map +1 -1
  32. package/dist/core/decoupling-eval.d.ts +53 -0
  33. package/dist/core/decoupling-eval.js +260 -0
  34. package/dist/core/decoupling-eval.js.map +1 -0
  35. package/dist/core/issue-publish-adapter.d.ts +50 -0
  36. package/dist/core/issue-publish-adapter.js +371 -0
  37. package/dist/core/issue-publish-adapter.js.map +1 -0
  38. package/dist/core/prepare-artifacts.d.ts +2 -0
  39. package/dist/core/prepare-artifacts.js +2 -0
  40. package/dist/core/prepare-artifacts.js.map +1 -1
  41. package/dist/core/prepare-readiness.d.ts +40 -0
  42. package/dist/core/prepare-readiness.js +268 -0
  43. package/dist/core/prepare-readiness.js.map +1 -0
  44. package/dist/core/prepare-sync.d.ts +2 -0
  45. package/dist/core/prepare-sync.js +8 -16
  46. package/dist/core/prepare-sync.js.map +1 -1
  47. package/dist/core/review-skill-policy.d.ts +74 -0
  48. package/dist/core/review-skill-policy.js +278 -0
  49. package/dist/core/review-skill-policy.js.map +1 -0
  50. package/dist/core/skill-classification.d.ts +13 -0
  51. package/dist/core/skill-classification.js +50 -0
  52. package/dist/core/skill-classification.js.map +1 -0
  53. package/dist/core/source-snapshot-adapters.d.ts +3 -0
  54. package/dist/core/source-snapshot-adapters.js.map +1 -1
  55. package/dist/core/stage-clarity-verification.js +58 -7
  56. package/dist/core/stage-clarity-verification.js.map +1 -1
  57. package/dist/core/task-decomposition.d.ts +64 -0
  58. package/dist/core/task-decomposition.js +325 -0
  59. package/dist/core/task-decomposition.js.map +1 -0
  60. package/dist/core/task-plan-adapter.d.ts +57 -0
  61. package/dist/core/task-plan-adapter.js +298 -0
  62. package/dist/core/task-plan-adapter.js.map +1 -0
  63. package/dist/core/template-registry.js +26 -7
  64. package/dist/core/template-registry.js.map +1 -1
  65. package/dist/core/validation-packet.d.ts +86 -0
  66. package/dist/core/validation-packet.js +313 -0
  67. package/dist/core/validation-packet.js.map +1 -0
  68. package/dist/core/work-ledger.d.ts +44 -0
  69. package/dist/core/work-ledger.js +88 -0
  70. package/dist/core/work-ledger.js.map +1 -0
  71. package/dist/core/work-provider-adapters.d.ts +110 -0
  72. package/dist/core/work-provider-adapters.js +484 -0
  73. package/dist/core/work-provider-adapters.js.map +1 -0
  74. package/dist/core/workflow-graph.d.ts +100 -0
  75. package/dist/core/workflow-graph.js +655 -0
  76. package/dist/core/workflow-graph.js.map +1 -0
  77. package/dist/core/workflow-orchestration-policy.d.ts +92 -0
  78. package/dist/core/workflow-orchestration-policy.js +215 -0
  79. package/dist/core/workflow-orchestration-policy.js.map +1 -0
  80. package/dist/core/workspace-conformance.js +55 -0
  81. package/dist/core/workspace-conformance.js.map +1 -1
  82. package/dist/core/workspace-layout.d.ts +3 -1
  83. package/dist/core/workspace-layout.js +4 -0
  84. package/dist/core/workspace-layout.js.map +1 -1
  85. package/package.json +2 -2
  86. package/schemas/zsk-config.schema.json +112 -1
  87. package/templates/module/frontend-module/CONTEXT.md +22 -0
  88. package/templates/module/frontend-module/design.md +1 -1
  89. package/templates/module/frontend-module/proposal.md +1 -1
  90. package/templates/module/frontend-module/spec.md +1 -1
  91. package/templates/module/frontend-module/tasks.md +14 -1
  92. package/templates/project-init/.zsk/CONTEXT.md +35 -0
  93. package/templates/project-init/.zsk/README.md +94 -10
  94. package/templates/project-init/.zsk/config.yaml +2 -0
  95. package/templates/project-init/.zsk/docs/CONFIG-SCHEMA.md +13 -1
  96. package/templates/project-init/.zsk/docs/PROJECT-CONFIG.md +25 -7
  97. package/templates/project-init/.zsk/docs/SYSTEM-SPEC.md +1 -0
  98. package/templates/project-init/.zsk/team.yaml +218 -0
  99. package/templates/project-init/.zsk/work.yaml +75 -0
  100. package/templates/project-init/.zsk/evidence/README.md +0 -21
  101. package/templates/project-init/.zsk/evidence/prepare/README.md +0 -22
  102. package/templates/project-init/.zsk/issues/README.md +0 -10
  103. package/templates/project-init/.zsk/templates/module/README.md +0 -13
  104. package/templates/project-init/.zsk/templates/module/design.md +0 -22
  105. package/templates/project-init/.zsk/templates/module/module.yaml +0 -15
  106. package/templates/project-init/.zsk/templates/module/proposal.md +0 -20
  107. package/templates/project-init/.zsk/templates/module/spec.md +0 -22
  108. package/templates/project-init/.zsk/templates/module/tasks.md +0 -16
@@ -0,0 +1,1769 @@
1
+ import pc from "picocolors";
2
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { join, relative, resolve } from "node:path";
4
+ import YAML from "yaml";
5
+ import { checkProjectConfig, readProjectConfig } from "../core/config.js";
6
+ import { resolvePath } from "../core/targets.js";
7
+ import { assignmentProviderRef, buildAssignmentProviderCommandPlan, buildProviderCommandPlan, commandDisplayWithMetadata, } from "../core/work-provider-adapters.js";
8
+ import { appendWorkEvents, latestWorkItem, readWorkEvents, resolveWorkLedgerPath } from "../core/work-ledger.js";
9
+ import { getWorkspacePath } from "../core/workspace-layout.js";
10
+ import { isPathInside, resolveProjectPath } from "../core/workspace-conformance.js";
11
+ import { refreshIssueIndexes, updateIssueStatusBlock } from "./issue.js";
12
+ export async function sync(opts = {}) {
13
+ if (!opts.dryRun) {
14
+ console.error(pc.red("✗ remote work sync is not enabled yet; rerun with --dry-run"));
15
+ process.exit(1);
16
+ }
17
+ const { target, config, workConfigPath, provider, providerConfig } = await loadWorkContext(opts, "sync");
18
+ const { ledgerPath } = await resolveWorkLedgerPath(target, config);
19
+ const events = await readWorkEvents(ledgerPath);
20
+ const now = new Date().toISOString();
21
+ const plan = buildSyncPlan({
22
+ target,
23
+ provider,
24
+ providerConfig,
25
+ workConfigPath,
26
+ ledgerPath,
27
+ events,
28
+ generatedAt: now,
29
+ });
30
+ const evidenceDir = resolve(target, getWorkspacePath(config, "evidenceRoot"), "work-sync", runId(now));
31
+ await mkdir(evidenceDir, { recursive: true });
32
+ const jsonPath = join(evidenceDir, "sync-plan.json");
33
+ const mdPath = join(evidenceDir, "sync-plan.md");
34
+ await writeFile(jsonPath, `${JSON.stringify(plan, null, 2)}\n`, "utf8");
35
+ await writeFile(mdPath, renderSyncPlan(plan), "utf8");
36
+ if (opts.json) {
37
+ console.log(JSON.stringify({ plan, artifacts: { json: jsonPath, markdown: mdPath } }, null, 2));
38
+ return;
39
+ }
40
+ console.log(pc.green("✓ zsk work sync dry-run completed"));
41
+ console.log(pc.dim(`provider: ${provider}`));
42
+ console.log(pc.dim(`adapter: ${plan.adapter ?? "unknown"}`));
43
+ console.log(pc.dim(`work config: ${workConfigPath}`));
44
+ console.log(pc.dim(`ledger: ${ledgerPath}${events.length === 0 ? " (no events)" : ""}`));
45
+ console.log(pc.dim(`events: ${plan.eventCount}; actionable: ${plan.actionableCount}; blocked: ${plan.blockedCount}`));
46
+ console.log(pc.dim(`provider commands: ${plan.providerCommandPlan.commandCount}; command blockers: ${plan.providerCommandPlan.blockedCount}`));
47
+ console.log(pc.dim("remote side effects: none"));
48
+ console.log(pc.dim(`sync plan: ${jsonPath}`));
49
+ console.log(pc.dim(`summary: ${mdPath}`));
50
+ }
51
+ export async function assign(opts = {}) {
52
+ if (!opts.item) {
53
+ console.error(pc.red("✗ missing work item id; pass --item <id>"));
54
+ process.exit(1);
55
+ }
56
+ if (!opts.agent && !opts.squad && !opts.role) {
57
+ console.error(pc.red("✗ assignment needs --agent, --squad, or --role"));
58
+ process.exit(1);
59
+ }
60
+ const { target, config, workConfigPath, provider, providerConfig } = await loadWorkContext(opts, "assign");
61
+ const { ledgerPath } = await resolveWorkLedgerPath(target, config);
62
+ const existingEvents = await readWorkEvents(ledgerPath);
63
+ const previousWorkItem = latestWorkItem(existingEvents, opts.item);
64
+ const teamConfigPath = resolveInsideProject(target, config.customize?.team ?? ".zsk/team.yaml", "team config");
65
+ const teamConfig = await readTeamConfig(teamConfigPath);
66
+ const assignment = resolveAssignment(opts, provider, teamConfig);
67
+ const now = new Date().toISOString();
68
+ const evidenceDir = resolve(target, getWorkspacePath(config, "evidenceRoot"), "work-assign", runId(now));
69
+ const jsonPath = join(evidenceDir, "assignment-plan.json");
70
+ const mdPath = join(evidenceDir, "assignment-plan.md");
71
+ const workItem = resolveAssignmentWorkItem(opts, providerConfig, previousWorkItem);
72
+ const providerConfigured = providerLooksConfigured(providerConfig);
73
+ const providerCommandPlan = buildAssignmentProviderCommandPlan({
74
+ provider,
75
+ providerConfig,
76
+ providerConfigured,
77
+ workItem,
78
+ assignment,
79
+ });
80
+ const assignmentEvents = buildAssignmentEvents({
81
+ target,
82
+ provider,
83
+ evidencePath: jsonPath,
84
+ assignedAt: now,
85
+ existingEvents,
86
+ previous: previousWorkItem,
87
+ workItem,
88
+ assignment,
89
+ providerCommandPlan,
90
+ });
91
+ const appendedCount = opts.dryRun ? 0 : assignmentEvents.length;
92
+ const plan = {
93
+ version: 1,
94
+ dryRun: Boolean(opts.dryRun),
95
+ provider,
96
+ adapter: providerConfig.adapter ?? null,
97
+ generatedAt: now,
98
+ target,
99
+ workConfig: workConfigPath,
100
+ teamConfig: teamConfigPath,
101
+ ledger: ledgerPath,
102
+ providerConfigured,
103
+ remoteSideEffects: "none",
104
+ localSideEffects: appendedCount > 0 ? "append-work-ledger" : "none",
105
+ appendedCount,
106
+ workItem,
107
+ assignment,
108
+ providerCommandPlan,
109
+ qualityGates: assignmentQualityGates(assignment, teamConfig, providerConfigured, providerCommandPlan),
110
+ };
111
+ await mkdir(evidenceDir, { recursive: true });
112
+ if (!opts.dryRun && assignmentEvents.length > 0) {
113
+ await appendWorkEvents(ledgerPath, assignmentEvents);
114
+ }
115
+ await writeFile(jsonPath, `${JSON.stringify(plan, null, 2)}\n`, "utf8");
116
+ await writeFile(mdPath, renderAssignmentPlan(plan), "utf8");
117
+ if (opts.json) {
118
+ console.log(JSON.stringify({ plan, artifacts: { json: jsonPath, markdown: mdPath } }, null, 2));
119
+ return;
120
+ }
121
+ console.log(pc.green(`✓ zsk work assign ${opts.dryRun ? "dry-run " : ""}completed`));
122
+ console.log(pc.dim(`provider: ${provider}`));
123
+ console.log(pc.dim(`work item: ${opts.item}`));
124
+ console.log(pc.dim(`assignment: ${assignment.mode}:${assignment.agent ?? assignment.squad ?? assignment.role}`));
125
+ console.log(pc.dim(`role: ${assignment.role}`));
126
+ console.log(pc.dim(`provider commands: ${plan.providerCommandPlan.commandCount}; command blockers: ${plan.providerCommandPlan.blockedCount}`));
127
+ console.log(pc.dim("remote side effects: none"));
128
+ console.log(pc.dim(`local side effects: ${plan.localSideEffects}`));
129
+ console.log(pc.dim(`ledger: ${ledgerPath}${plan.appendedCount > 0 ? ` (${plan.appendedCount} appended)` : ""}`));
130
+ console.log(pc.dim(`assignment plan: ${jsonPath}`));
131
+ console.log(pc.dim(`summary: ${mdPath}`));
132
+ }
133
+ export async function importStatus(opts = {}) {
134
+ if (!opts.file) {
135
+ console.error(pc.red("✗ missing provider status file; pass --file <path>"));
136
+ process.exit(1);
137
+ }
138
+ const { target, config, workConfigPath, provider, providerConfig } = await loadWorkContext(opts, "import-status");
139
+ const inputFile = resolveInsideProject(target, opts.file, "provider status file");
140
+ const { ledgerPath } = await resolveWorkLedgerPath(target, config);
141
+ const existingEvents = await readWorkEvents(ledgerPath);
142
+ const records = await readProviderStatusRecords(inputFile);
143
+ const inverseStatusMapping = invertMapping(providerConfig.statusMapping ?? {});
144
+ const now = new Date().toISOString();
145
+ const baseItems = records.map((record) => {
146
+ const providerStatus = record.status ?? null;
147
+ const localStatus = providerStatus ? inverseStatusMapping[providerStatus] ?? null : null;
148
+ const blockedReasons = [];
149
+ const localId = record.localId ?? record.workItemId ?? record.id ?? null;
150
+ if (!localId)
151
+ blockedReasons.push("localId");
152
+ if (!providerStatus)
153
+ blockedReasons.push("status");
154
+ if (providerStatus && !localStatus)
155
+ blockedReasons.push(`unmapped status: ${providerStatus}`);
156
+ return {
157
+ localId,
158
+ remoteId: record.remoteId ?? null,
159
+ providerStatus,
160
+ localStatus,
161
+ updatedAt: record.updatedAt ?? null,
162
+ blockedReasons,
163
+ artifactTargets: [],
164
+ updatedArtifacts: [],
165
+ artifactNotes: [],
166
+ };
167
+ });
168
+ const items = await Promise.all(baseItems.map(async (item, index) => {
169
+ const record = records[index];
170
+ const previous = item.localId ? latestWorkItem(existingEvents, item.localId) : undefined;
171
+ const artifactPlan = await planLocalArtifactTargets({
172
+ target,
173
+ config,
174
+ record,
175
+ previous,
176
+ localId: item.localId,
177
+ });
178
+ return {
179
+ ...item,
180
+ artifactTargets: artifactPlan.targets,
181
+ artifactNotes: artifactPlan.notes,
182
+ };
183
+ }));
184
+ const blockedCount = items.filter((item) => item.blockedReasons.length > 0).length;
185
+ const evidenceDir = resolve(target, getWorkspacePath(config, "evidenceRoot"), "work-status-import", runId(now));
186
+ const jsonPath = join(evidenceDir, "status-import-plan.json");
187
+ const mdPath = join(evidenceDir, "status-import-plan.md");
188
+ const artifactUpdateCount = opts.dryRun
189
+ ? 0
190
+ : await applyStatusImportArtifacts({
191
+ target,
192
+ config,
193
+ provider,
194
+ importedAt: now,
195
+ items,
196
+ });
197
+ const importedEvents = opts.dryRun
198
+ ? []
199
+ : buildStatusImportEvents({
200
+ target,
201
+ provider,
202
+ inputFile,
203
+ evidencePath: jsonPath,
204
+ importedAt: now,
205
+ existingEvents,
206
+ items,
207
+ });
208
+ if (!opts.dryRun && importedEvents.length > 0) {
209
+ await appendWorkEvents(ledgerPath, importedEvents);
210
+ }
211
+ const localSideEffects = resolveStatusImportLocalSideEffects(Boolean(opts.dryRun), importedEvents.length, artifactUpdateCount);
212
+ const plan = {
213
+ version: 1,
214
+ dryRun: Boolean(opts.dryRun),
215
+ provider,
216
+ adapter: providerConfig.adapter ?? null,
217
+ generatedAt: now,
218
+ target,
219
+ workConfig: workConfigPath,
220
+ inputFile,
221
+ ledger: ledgerPath,
222
+ remoteSideEffects: "none",
223
+ localSideEffects,
224
+ itemCount: items.length,
225
+ mappedCount: items.length - blockedCount,
226
+ blockedCount,
227
+ appendedCount: importedEvents.length,
228
+ artifactUpdateCount,
229
+ items,
230
+ };
231
+ await mkdir(evidenceDir, { recursive: true });
232
+ await writeFile(jsonPath, `${JSON.stringify(plan, null, 2)}\n`, "utf8");
233
+ await writeFile(mdPath, renderStatusImportPlan(plan), "utf8");
234
+ if (opts.json) {
235
+ console.log(JSON.stringify({ plan, artifacts: { json: jsonPath, markdown: mdPath } }, null, 2));
236
+ return;
237
+ }
238
+ console.log(pc.green(`✓ zsk work import-status ${opts.dryRun ? "dry-run " : ""}completed`));
239
+ console.log(pc.dim(`provider: ${provider}`));
240
+ console.log(pc.dim(`input: ${inputFile}`));
241
+ console.log(pc.dim(`items: ${plan.itemCount}; mapped: ${plan.mappedCount}; blocked: ${plan.blockedCount}`));
242
+ console.log(pc.dim("remote side effects: none"));
243
+ console.log(pc.dim(`local side effects: ${plan.localSideEffects}`));
244
+ console.log(pc.dim(`ledger: ${ledgerPath}${plan.appendedCount > 0 ? ` (${plan.appendedCount} appended)` : ""}`));
245
+ if (plan.artifactUpdateCount > 0)
246
+ console.log(pc.dim(`artifacts updated: ${plan.artifactUpdateCount}`));
247
+ console.log(pc.dim(`status import plan: ${jsonPath}`));
248
+ console.log(pc.dim(`summary: ${mdPath}`));
249
+ }
250
+ export async function importExperts(opts = {}) {
251
+ if (!opts.file) {
252
+ console.error(pc.red("✗ missing provider expert export; pass --file <path>"));
253
+ process.exit(1);
254
+ }
255
+ const { target, config, workConfigPath, provider, providerConfig } = await loadWorkContext(opts, "import-experts");
256
+ const teamConfigPath = resolveInsideProject(target, config.customize?.team ?? ".zsk/team.yaml", "team config");
257
+ const teamConfig = await readTeamConfig(teamConfigPath);
258
+ const inputFile = resolveInsideProject(target, opts.file, "provider expert export");
259
+ const outputFile = resolveInsideProject(target, resolveExpertCatalogOutput(provider, providerConfig, teamConfig, opts.out), "provider expert catalog");
260
+ const outputRel = projectRelative(target, outputFile);
261
+ const raw = YAML.parse(await readFile(inputFile, "utf8"));
262
+ const generatedAt = new Date().toISOString();
263
+ const items = providerExpertPlanItems(raw);
264
+ const validItems = items.filter((item) => item.blockedReasons.length === 0);
265
+ const catalog = buildProviderExpertCatalog(provider, generatedAt, validItems);
266
+ const blockedCount = items.length - validItems.length;
267
+ const outputConfigured = expertCatalogPathIsConfigured(outputRel, provider, providerConfig, teamConfig);
268
+ const evidenceDir = resolve(target, getWorkspacePath(config, "evidenceRoot"), "work-expert-import", runId(generatedAt));
269
+ const jsonPath = join(evidenceDir, "expert-import-plan.json");
270
+ const mdPath = join(evidenceDir, "expert-import-plan.md");
271
+ const plan = {
272
+ version: 1,
273
+ dryRun: Boolean(opts.dryRun),
274
+ provider,
275
+ adapter: providerConfig.adapter ?? null,
276
+ generatedAt,
277
+ target,
278
+ workConfig: workConfigPath,
279
+ teamConfig: teamConfigPath,
280
+ inputFile,
281
+ outputFile,
282
+ outputConfigured,
283
+ remoteSideEffects: "none",
284
+ localSideEffects: !opts.dryRun && validItems.length > 0 ? "write-provider-expert-catalog" : "none",
285
+ itemCount: items.length,
286
+ normalizedCount: validItems.length,
287
+ blockedCount,
288
+ writtenCount: opts.dryRun ? 0 : validItems.length,
289
+ catalog,
290
+ items,
291
+ qualityGates: expertImportQualityGates(items, validItems.length, outputConfigured, Boolean(opts.dryRun)),
292
+ };
293
+ await mkdir(evidenceDir, { recursive: true });
294
+ if (!opts.dryRun && validItems.length > 0) {
295
+ await mkdir(resolve(outputFile, ".."), { recursive: true });
296
+ await writeFile(outputFile, YAML.stringify(catalog), "utf8");
297
+ }
298
+ await writeFile(jsonPath, `${JSON.stringify(plan, null, 2)}\n`, "utf8");
299
+ await writeFile(mdPath, renderExpertImportPlan(plan), "utf8");
300
+ if (opts.json) {
301
+ console.log(JSON.stringify({ plan, artifacts: { json: jsonPath, markdown: mdPath, catalog: outputFile } }, null, 2));
302
+ return;
303
+ }
304
+ console.log(pc.green(`✓ zsk work import-experts ${opts.dryRun ? "dry-run " : ""}completed`));
305
+ console.log(pc.dim(`provider: ${provider}`));
306
+ console.log(pc.dim(`input: ${inputFile}`));
307
+ console.log(pc.dim(`catalog: ${outputFile}${outputConfigured ? "" : " (auto-discovered default path)"}`));
308
+ console.log(pc.dim(`experts: ${plan.itemCount}; normalized: ${plan.normalizedCount}; blocked: ${plan.blockedCount}`));
309
+ console.log(pc.dim("remote side effects: none"));
310
+ console.log(pc.dim(`local side effects: ${plan.localSideEffects}`));
311
+ console.log(pc.dim(`expert import plan: ${jsonPath}`));
312
+ console.log(pc.dim(`summary: ${mdPath}`));
313
+ }
314
+ export async function emitTasks(opts = {}) {
315
+ const { target, config, workConfigPath, provider, providerConfig } = await loadWorkContext(opts, "emit-tasks");
316
+ const moduleId = opts.module ?? inferModuleFromTaskPath(target, config, opts.tasks);
317
+ if (!moduleId) {
318
+ console.error(pc.red("✗ missing module id; pass -m <module-id> or --tasks <module-tasks.md>"));
319
+ process.exit(1);
320
+ }
321
+ const taskArtifactRel = opts.tasks ?? join(getWorkspacePath(config, "moduleRoot", { module: moduleId }), "tasks.md");
322
+ const taskArtifactPath = resolveInsideProject(target, taskArtifactRel, "tasks artifact");
323
+ if (!isAllowedTaskArtifact(target, config, taskArtifactPath)) {
324
+ console.error(pc.red(`✗ tasks artifact must be a module tasks.md file: ${taskArtifactRel}`));
325
+ process.exit(1);
326
+ }
327
+ const { ledgerPath } = await resolveWorkLedgerPath(target, config);
328
+ const existingEvents = await readWorkEvents(ledgerPath);
329
+ const now = new Date().toISOString();
330
+ const evidenceDir = resolve(target, getWorkspacePath(config, "evidenceRoot"), "work-task-emit", runId(now));
331
+ const jsonPath = join(evidenceDir, "task-emit-plan.json");
332
+ const mdPath = join(evidenceDir, "task-emit-plan.md");
333
+ const tasksContent = await readFile(taskArtifactPath, "utf8");
334
+ const parsedItems = parseTaskWorkItems({
335
+ target,
336
+ providerConfig,
337
+ taskArtifactPath,
338
+ content: tasksContent,
339
+ moduleId,
340
+ projectId: opts.project,
341
+ stage: opts.stage ?? "task",
342
+ existingEvents,
343
+ });
344
+ const events = buildTaskEmitEvents({
345
+ items: parsedItems,
346
+ emittedAt: now,
347
+ evidencePath: projectRelative(target, jsonPath),
348
+ });
349
+ const appendedCount = opts.dryRun ? 0 : events.length;
350
+ const plan = {
351
+ version: 1,
352
+ dryRun: Boolean(opts.dryRun),
353
+ provider,
354
+ adapter: providerConfig.adapter ?? null,
355
+ generatedAt: now,
356
+ target,
357
+ workConfig: workConfigPath,
358
+ ledger: ledgerPath,
359
+ tasksArtifact: projectRelative(target, taskArtifactPath),
360
+ remoteSideEffects: "none",
361
+ localSideEffects: appendedCount > 0 ? "append-work-ledger" : "none",
362
+ itemCount: parsedItems.length,
363
+ appendableCount: events.length,
364
+ blockedCount: parsedItems.filter((item) => item.blockedReasons.length > 0).length,
365
+ skippedCount: parsedItems.filter((item) => item.skipReason).length,
366
+ appendedCount,
367
+ items: parsedItems,
368
+ qualityGates: taskEmitQualityGates(parsedItems, events.length, Boolean(opts.dryRun)),
369
+ };
370
+ await mkdir(evidenceDir, { recursive: true });
371
+ if (!opts.dryRun && events.length > 0) {
372
+ await appendWorkEvents(ledgerPath, events);
373
+ }
374
+ await writeFile(jsonPath, `${JSON.stringify(plan, null, 2)}\n`, "utf8");
375
+ await writeFile(mdPath, renderTaskEmitPlan(plan), "utf8");
376
+ if (opts.json) {
377
+ console.log(JSON.stringify({ plan, artifacts: { json: jsonPath, markdown: mdPath } }, null, 2));
378
+ return;
379
+ }
380
+ console.log(pc.green(`✓ zsk work emit-tasks ${opts.dryRun ? "dry-run " : ""}completed`));
381
+ console.log(pc.dim(`provider: ${provider}`));
382
+ console.log(pc.dim(`tasks: ${taskArtifactPath}`));
383
+ console.log(pc.dim(`items: ${plan.itemCount}; appendable: ${plan.appendableCount}; blocked: ${plan.blockedCount}; skipped: ${plan.skippedCount}`));
384
+ console.log(pc.dim("remote side effects: none"));
385
+ console.log(pc.dim(`local side effects: ${plan.localSideEffects}`));
386
+ console.log(pc.dim(`ledger: ${ledgerPath}${plan.appendedCount > 0 ? ` (${plan.appendedCount} appended)` : ""}`));
387
+ console.log(pc.dim(`task emit plan: ${jsonPath}`));
388
+ console.log(pc.dim(`summary: ${mdPath}`));
389
+ }
390
+ function buildSyncPlan(input) {
391
+ const items = input.events.map((event, index) => planItem(index + 1, event, input.provider, input.providerConfig));
392
+ const providerConfigured = providerLooksConfigured(input.providerConfig);
393
+ const blockedCount = items.filter((item) => !item.remoteReady).length;
394
+ const actionableCount = items.length - blockedCount;
395
+ const providerCommandPlan = buildProviderCommandPlan({
396
+ provider: input.provider,
397
+ providerConfig: input.providerConfig,
398
+ providerConfigured,
399
+ items,
400
+ events: input.events,
401
+ });
402
+ return {
403
+ version: 1,
404
+ dryRun: true,
405
+ provider: input.provider,
406
+ adapter: input.providerConfig.adapter ?? null,
407
+ generatedAt: input.generatedAt,
408
+ target: input.target,
409
+ workConfig: input.workConfigPath,
410
+ ledger: input.ledgerPath,
411
+ providerConfigured,
412
+ remoteSideEffects: "none",
413
+ eventCount: input.events.length,
414
+ actionableCount,
415
+ blockedCount,
416
+ items,
417
+ providerCommandPlan,
418
+ qualityGates: [
419
+ {
420
+ id: "provider-configured",
421
+ status: providerConfigured ? "pass" : "fail",
422
+ detail: providerConfigured ? "provider has a non-placeholder workspace/config" : "provider config is placeholder-only",
423
+ },
424
+ {
425
+ id: "dry-run-only",
426
+ status: "pass",
427
+ detail: "no provider command or API call was executed",
428
+ },
429
+ {
430
+ id: "event-schema",
431
+ status: blockedCount === 0 ? "pass" : "fail",
432
+ detail: blockedCount === 0 ? "all events have required sync fields" : `${blockedCount} event(s) need missing fields before remote sync`,
433
+ },
434
+ {
435
+ id: "provider-command-plan",
436
+ status: providerCommandPlan.blockedCount === 0 ? "pass" : "fail",
437
+ detail: providerCommandPlan.blockedCount === 0
438
+ ? `${providerCommandPlan.commandCount} provider command(s) planned without execution`
439
+ : `${providerCommandPlan.blockedCount} provider command(s) need missing fields before execution`,
440
+ },
441
+ ],
442
+ };
443
+ }
444
+ function planItem(line, event, provider, providerConfig) {
445
+ const workItem = event.workItem ?? {};
446
+ const localStatus = event.status?.to ?? workItem.status ?? null;
447
+ const blockedReasons = requiredWorkItemGaps(event);
448
+ return {
449
+ line,
450
+ event: event.event ?? "unknown",
451
+ action: actionForEvent(event.event),
452
+ localId: workItem.id ?? null,
453
+ project: idOf(workItem.project),
454
+ module: idOf(workItem.module),
455
+ stage: workItem.stage ?? event.producer?.stage ?? null,
456
+ localStatus,
457
+ providerStatus: localStatus ? providerConfig.statusMapping?.[localStatus] ?? null : null,
458
+ remoteRef: workItem.externalRefs?.[provider] ?? null,
459
+ remoteReady: blockedReasons.length === 0,
460
+ blockedReasons,
461
+ };
462
+ }
463
+ function actionForEvent(event) {
464
+ switch (event) {
465
+ case "work.provider_status.imported":
466
+ return "record-provider-status-import";
467
+ case "work.item.upserted":
468
+ return "create-or-update-work-item";
469
+ case "work.item.linked":
470
+ return "update-links";
471
+ case "work.item.status_changed":
472
+ return "update-status";
473
+ case "work.assignment.requested":
474
+ return "request-assignment";
475
+ case "work.assignment.accepted":
476
+ return "confirm-assignment";
477
+ case "work.comment.added":
478
+ return "add-comment";
479
+ case "work.item.verified":
480
+ return "mark-verified";
481
+ case "work.item.blocked":
482
+ return "mark-blocked";
483
+ default:
484
+ return "inspect-only";
485
+ }
486
+ }
487
+ function requiredWorkItemGaps(event) {
488
+ if (event.event === "work.provider_status.imported") {
489
+ return requiredProviderStatusImportGaps(event);
490
+ }
491
+ const gaps = [];
492
+ if (!event.event)
493
+ gaps.push("event");
494
+ if (!event.producer?.skill)
495
+ gaps.push("producer.skill");
496
+ if (!event.producer?.stage)
497
+ gaps.push("producer.stage");
498
+ const workItem = event.workItem;
499
+ if (!workItem?.id)
500
+ gaps.push("workItem.id");
501
+ if (!workItem?.type)
502
+ gaps.push("workItem.type");
503
+ if (!workItem?.title)
504
+ gaps.push("workItem.title");
505
+ if (!idOf(workItem?.project))
506
+ gaps.push("workItem.project");
507
+ if (!idOf(workItem?.module))
508
+ gaps.push("workItem.module");
509
+ if (!workItem?.stage && !event.producer?.stage)
510
+ gaps.push("workItem.stage");
511
+ if (!workItem?.status && !event.status?.to)
512
+ gaps.push("workItem.status");
513
+ if (!Array.isArray(workItem?.sourceArtifacts) || workItem.sourceArtifacts.length === 0) {
514
+ gaps.push("workItem.sourceArtifacts");
515
+ }
516
+ return gaps;
517
+ }
518
+ function requiredProviderStatusImportGaps(event) {
519
+ const gaps = [];
520
+ if (!event.event)
521
+ gaps.push("event");
522
+ if (!event.producer?.skill)
523
+ gaps.push("producer.skill");
524
+ if (!event.producer?.stage)
525
+ gaps.push("producer.stage");
526
+ if (!event.workItem?.id)
527
+ gaps.push("workItem.id");
528
+ if (!event.status?.to)
529
+ gaps.push("status.to");
530
+ if (typeof event.status?.providerStatus !== "string")
531
+ gaps.push("status.providerStatus");
532
+ return gaps;
533
+ }
534
+ async function readWorkConfig(path) {
535
+ try {
536
+ const raw = await readFile(path, "utf8");
537
+ const parsed = YAML.parse(raw);
538
+ return isRecord(parsed) ? parsed : {};
539
+ }
540
+ catch {
541
+ console.error(pc.red(`✗ work config not found: ${path}`));
542
+ process.exit(1);
543
+ }
544
+ }
545
+ function renderSyncPlan(plan) {
546
+ const rows = plan.items.map((item) => [
547
+ item.line,
548
+ item.event,
549
+ item.action,
550
+ item.localId ?? "",
551
+ item.project ?? "",
552
+ item.module ?? "",
553
+ item.localStatus ?? "",
554
+ item.providerStatus ?? "",
555
+ item.remoteReady ? "yes" : item.blockedReasons.join(", "),
556
+ ].join(" | "));
557
+ const commandRows = plan.providerCommandPlan.commands.map((command) => [
558
+ command.line,
559
+ command.action,
560
+ command.operation,
561
+ command.ready ? "yes" : command.blockedReasons.join(", "),
562
+ commandDisplayWithMetadata(command),
563
+ ].join(" | "));
564
+ return [
565
+ "# Work Sync Dry Run",
566
+ "",
567
+ `- Provider: ${plan.provider}`,
568
+ `- Adapter: ${plan.adapter ?? "unknown"}`,
569
+ `- Generated at: ${plan.generatedAt}`,
570
+ `- Work config: ${plan.workConfig}`,
571
+ `- Ledger: ${plan.ledger}`,
572
+ `- Remote side effects: ${plan.remoteSideEffects}`,
573
+ `- Events: ${plan.eventCount}`,
574
+ `- Actionable: ${plan.actionableCount}`,
575
+ `- Blocked: ${plan.blockedCount}`,
576
+ `- Provider command executable: ${plan.providerCommandPlan.executable}`,
577
+ `- Provider commands: ${plan.providerCommandPlan.commandCount}`,
578
+ `- Provider command blockers: ${plan.providerCommandPlan.blockedCount}`,
579
+ "",
580
+ "## Quality Gates",
581
+ "",
582
+ ...plan.qualityGates.map((gate) => `- ${gate.id}: ${gate.status} - ${gate.detail}`),
583
+ "",
584
+ "## Planned Provider Operations",
585
+ "",
586
+ "line | event | action | local id | project | module | local status | provider status | remote ready",
587
+ "--- | --- | --- | --- | --- | --- | --- | --- | ---",
588
+ ...(rows.length > 0 ? rows : [" | | | | | | | | no events"]),
589
+ "",
590
+ "## Provider Command Plan",
591
+ "",
592
+ "line | action | operation | ready | command / note",
593
+ "--- | --- | --- | --- | ---",
594
+ ...(commandRows.length > 0 ? commandRows : [" | | | | no provider commands"]),
595
+ "",
596
+ ].join("\n");
597
+ }
598
+ async function loadWorkContext(opts, action) {
599
+ const target = opts.target ? resolvePath(opts.target) : process.cwd();
600
+ const configCheck = await checkProjectConfig(target);
601
+ for (const warning of configCheck.warnings) {
602
+ console.warn(pc.yellow(`! ${warning.file}: ${warning.message}`));
603
+ }
604
+ if (!configCheck.ok) {
605
+ console.error(pc.red(`✗ zsk work ${action} stopped: config check failed`));
606
+ process.exit(1);
607
+ }
608
+ const config = await readProjectConfig(target);
609
+ const workConfigRel = config.customize?.work ?? ".zsk/work.yaml";
610
+ const workConfigPath = resolveInsideProject(target, workConfigRel, "work config");
611
+ const workConfig = await readWorkConfig(workConfigPath);
612
+ const provider = opts.provider ?? workConfig.defaultProvider ?? "multica";
613
+ const providerConfig = workConfig.providers?.[provider];
614
+ if (!providerConfig) {
615
+ console.error(pc.red(`✗ work provider not configured: ${provider}`));
616
+ console.error(pc.dim(`configured providers: ${Object.keys(workConfig.providers ?? {}).join(", ") || "none"}`));
617
+ process.exit(1);
618
+ }
619
+ return { target, config, workConfigPath, workConfig, provider, providerConfig };
620
+ }
621
+ function resolveAssignment(opts, provider, teamConfig) {
622
+ const mode = opts.agent ? "agent" : opts.squad ? "squad" : "role";
623
+ const inferredRole = opts.role ?? inferRole(opts, teamConfig);
624
+ if (!inferredRole) {
625
+ console.error(pc.red("✗ unable to infer assignment role; pass --role <id>"));
626
+ process.exit(1);
627
+ }
628
+ if (!teamConfig.roles?.[inferredRole]) {
629
+ console.error(pc.red(`✗ unknown role in .zsk/team.yaml: ${inferredRole}`));
630
+ process.exit(1);
631
+ }
632
+ if (opts.agent && !teamConfig.agents?.[opts.agent]) {
633
+ console.error(pc.red(`✗ unknown agent in .zsk/team.yaml: ${opts.agent}`));
634
+ process.exit(1);
635
+ }
636
+ if (opts.squad && !teamConfig.squads?.[opts.squad]) {
637
+ console.error(pc.red(`✗ unknown squad in .zsk/team.yaml: ${opts.squad}`));
638
+ process.exit(1);
639
+ }
640
+ const providerAgent = opts.agent
641
+ ? teamConfig.agents?.[opts.agent]?.providerRefs?.[provider]?.agent ?? null
642
+ : opts.squad
643
+ ? providerAgentForSquad(opts.squad, provider, teamConfig)
644
+ : null;
645
+ return {
646
+ mode,
647
+ role: inferredRole,
648
+ agent: opts.agent ?? null,
649
+ squad: opts.squad ?? null,
650
+ providerAgent,
651
+ rationale: opts.reason ?? "assignment requested by zsk work assign",
652
+ };
653
+ }
654
+ function inferRole(opts, teamConfig) {
655
+ if (opts.agent) {
656
+ const roles = teamConfig.agents?.[opts.agent]?.roles ?? [];
657
+ return roles.length === 1 ? roles[0] ?? null : null;
658
+ }
659
+ if (opts.squad) {
660
+ const leader = teamConfig.squads?.[opts.squad]?.leader;
661
+ const roles = leader ? teamConfig.agents?.[leader]?.roles ?? [] : [];
662
+ return roles[0] ?? null;
663
+ }
664
+ return opts.role ?? null;
665
+ }
666
+ function providerAgentForSquad(squad, provider, teamConfig) {
667
+ const leader = teamConfig.squads?.[squad]?.leader;
668
+ if (!leader)
669
+ return null;
670
+ return teamConfig.agents?.[leader]?.providerRefs?.[provider]?.agent ?? null;
671
+ }
672
+ function resolveAssignmentWorkItem(opts, providerConfig, previous) {
673
+ const moduleId = opts.module ?? idOf(previous?.module);
674
+ return {
675
+ id: opts.item,
676
+ project: resolveWorkProject(opts.project, moduleId, providerConfig) ?? idOf(previous?.project),
677
+ module: moduleId ?? null,
678
+ stage: opts.stage ?? previous?.stage ?? null,
679
+ };
680
+ }
681
+ function resolveWorkProject(project, moduleId, providerConfig) {
682
+ if (project?.trim())
683
+ return project.trim();
684
+ const module = moduleId?.trim();
685
+ if (!module)
686
+ return null;
687
+ const mapped = providerConfig.projectMapping?.projects?.[module];
688
+ if (mapped?.trim())
689
+ return mapped.trim();
690
+ return providerConfig.projectMapping?.strategy === "module-to-project" ? module : null;
691
+ }
692
+ function inferModuleFromTaskPath(target, config, taskPath) {
693
+ if (!taskPath)
694
+ return null;
695
+ const resolved = resolveProjectPath(target, taskPath);
696
+ const modulesRoot = resolve(target, getWorkspacePath(config, "modulesRoot"));
697
+ if (!isPathInside(modulesRoot, resolved))
698
+ return null;
699
+ const [moduleId] = toPosix(relative(modulesRoot, resolved)).split("/");
700
+ return moduleId?.trim() || null;
701
+ }
702
+ function assignmentQualityGates(assignment, teamConfig, providerConfigured, providerCommandPlan) {
703
+ return [
704
+ {
705
+ id: "provider-configured",
706
+ status: providerConfigured ? "pass" : "fail",
707
+ detail: providerConfigured ? "provider has a non-placeholder workspace/config" : "provider config is placeholder-only",
708
+ },
709
+ {
710
+ id: "role-exists",
711
+ status: teamConfig.roles?.[assignment.role] ? "pass" : "fail",
712
+ detail: assignment.role,
713
+ },
714
+ {
715
+ id: "assignee-exists",
716
+ status: assignment.mode === "role" ||
717
+ (assignment.agent ? Boolean(teamConfig.agents?.[assignment.agent]) : Boolean(teamConfig.squads?.[assignment.squad ?? ""]))
718
+ ? "pass"
719
+ : "fail",
720
+ detail: assignment.agent ?? assignment.squad ?? assignment.role,
721
+ },
722
+ {
723
+ id: "provider-command-plan",
724
+ status: providerCommandPlan.blockedCount === 0 ? "pass" : "fail",
725
+ detail: providerCommandPlan.blockedCount === 0
726
+ ? `${providerCommandPlan.commandCount} provider command(s) planned without execution`
727
+ : `${providerCommandPlan.blockedCount} provider command blocker(s): ${providerCommandPlan.commands.flatMap((command) => command.blockedReasons).join(", ")}`,
728
+ },
729
+ {
730
+ id: "provider-side-effects",
731
+ status: "pass",
732
+ detail: "no provider command or API call was executed; non-dry-run only appends a local assignment event",
733
+ },
734
+ ];
735
+ }
736
+ function renderAssignmentPlan(plan) {
737
+ const commandRows = plan.providerCommandPlan.commands.map((command) => [
738
+ command.line,
739
+ command.action,
740
+ command.operation,
741
+ command.ready ? "yes" : command.blockedReasons.join(", "),
742
+ commandDisplayWithMetadata(command),
743
+ ].join(" | "));
744
+ return [
745
+ `# Work Assignment${plan.dryRun ? " Dry Run" : ""}`,
746
+ "",
747
+ `- Provider: ${plan.provider}`,
748
+ `- Adapter: ${plan.adapter ?? "unknown"}`,
749
+ `- Ledger: ${plan.ledger}`,
750
+ `- Work item: ${plan.workItem.id}`,
751
+ `- Project: ${plan.workItem.project ?? ""}`,
752
+ `- Module: ${plan.workItem.module ?? ""}`,
753
+ `- Stage: ${plan.workItem.stage ?? ""}`,
754
+ `- Mode: ${plan.assignment.mode}`,
755
+ `- Role: ${plan.assignment.role}`,
756
+ `- Agent: ${plan.assignment.agent ?? ""}`,
757
+ `- Squad: ${plan.assignment.squad ?? ""}`,
758
+ `- Provider agent: ${plan.assignment.providerAgent ?? ""}`,
759
+ `- Rationale: ${plan.assignment.rationale}`,
760
+ `- Remote side effects: ${plan.remoteSideEffects}`,
761
+ `- Local side effects: ${plan.localSideEffects}`,
762
+ `- Appended: ${plan.appendedCount}`,
763
+ `- Provider command executable: ${plan.providerCommandPlan.executable}`,
764
+ `- Provider commands: ${plan.providerCommandPlan.commandCount}`,
765
+ `- Provider command blockers: ${plan.providerCommandPlan.blockedCount}`,
766
+ "",
767
+ "## Quality Gates",
768
+ "",
769
+ ...plan.qualityGates.map((gate) => `- ${gate.id}: ${gate.status} - ${gate.detail}`),
770
+ "",
771
+ "## Provider Command Plan",
772
+ "",
773
+ "line | action | operation | ready | command / note",
774
+ "--- | --- | --- | --- | ---",
775
+ ...(commandRows.length > 0 ? commandRows : [" | | | | no provider commands"]),
776
+ "",
777
+ ].join("\n");
778
+ }
779
+ async function readTeamConfig(path) {
780
+ try {
781
+ const raw = await readFile(path, "utf8");
782
+ const parsed = YAML.parse(raw);
783
+ return isRecord(parsed) ? parsed : {};
784
+ }
785
+ catch {
786
+ console.error(pc.red(`✗ team config not found: ${path}`));
787
+ process.exit(1);
788
+ }
789
+ }
790
+ async function readProviderStatusRecords(path) {
791
+ try {
792
+ const raw = await readFile(path, "utf8");
793
+ const parsed = YAML.parse(raw);
794
+ if (Array.isArray(parsed))
795
+ return parsed.filter(isRecord);
796
+ if (isRecord(parsed) && Array.isArray(parsed.items))
797
+ return parsed.items.filter(isRecord);
798
+ if (isRecord(parsed))
799
+ return [parsed];
800
+ return [];
801
+ }
802
+ catch {
803
+ console.error(pc.red(`✗ provider status file not found: ${path}`));
804
+ process.exit(1);
805
+ }
806
+ }
807
+ function providerExpertPlanItems(value) {
808
+ return providerExpertRecords(value).map((record, index) => {
809
+ const sourceId = firstString(record.id, record.agentId, record.providerAgent, record.agent, record.username, record.handle, record.slug, record.name);
810
+ const providerAgent = firstString(record.providerAgent, record.agent, record.username, record.handle, record.slug, record.id, record.name);
811
+ const id = normalizeExpertId(firstString(record.id, record.agentId, record.slug, record.username, record.handle, record.name, providerAgent) ?? `expert-${index + 1}`);
812
+ const roles = uniqueStrings([
813
+ ...stringListOf(record.roles),
814
+ ...stringListOf(record.role),
815
+ ...stringListOf(record.roleIds),
816
+ ...stringListOf(record.roleNames),
817
+ ]);
818
+ const stages = uniqueStrings([
819
+ ...stringListOf(record.stages),
820
+ ...stringListOf(record.stage),
821
+ ...stringListOf(record.stageIds),
822
+ ...stringListOf(record.stageNames),
823
+ ]);
824
+ const modules = uniqueStrings([
825
+ ...stringListOf(record.modules),
826
+ ...stringListOf(record.module),
827
+ ...stringListOf(record.moduleIds),
828
+ ...stringListOf(record.projects),
829
+ ...stringListOf(record.projectIds),
830
+ ]);
831
+ const subagentTypes = uniqueStrings([
832
+ ...stringListOf(record.subagentTypes),
833
+ ...stringListOf(record.subagents),
834
+ ...stringListOf(record.agentTypes),
835
+ ...stringListOf(record.capabilities),
836
+ ]);
837
+ const priority = numberOf(record.priority);
838
+ const capacity = stringOf(record.capacity) ?? numberOf(record.capacity);
839
+ const blockedReasons = [];
840
+ if (!id)
841
+ blockedReasons.push("expert.id");
842
+ if (!providerAgent)
843
+ blockedReasons.push("expert.providerAgent");
844
+ if (roles.length === 0)
845
+ blockedReasons.push("expert.roles");
846
+ return {
847
+ sourceId,
848
+ id,
849
+ providerAgent,
850
+ roles,
851
+ stages,
852
+ modules,
853
+ subagentTypes,
854
+ capacity,
855
+ priority,
856
+ blockedReasons,
857
+ };
858
+ });
859
+ }
860
+ function providerExpertRecords(value) {
861
+ if (Array.isArray(value))
862
+ return value.filter(isRecord);
863
+ if (!isRecord(value))
864
+ return [];
865
+ for (const key of ["experts", "agents", "members", "users", "items", "records", "data"]) {
866
+ const item = value[key];
867
+ if (Array.isArray(item))
868
+ return item.filter(isRecord);
869
+ if (isRecord(item)) {
870
+ return Object.entries(item)
871
+ .filter(([, record]) => isRecord(record))
872
+ .map(([id, record]) => ({ id, ...record }));
873
+ }
874
+ }
875
+ return [value];
876
+ }
877
+ function buildProviderExpertCatalog(provider, generatedAt, items) {
878
+ return {
879
+ version: 1,
880
+ provider,
881
+ generatedAt,
882
+ experts: items.map((item) => compactRecord({
883
+ id: item.id,
884
+ providerAgent: item.providerAgent,
885
+ roles: item.roles,
886
+ stages: item.stages,
887
+ modules: item.modules,
888
+ subagentTypes: item.subagentTypes,
889
+ capacity: item.capacity,
890
+ priority: item.priority,
891
+ providerRefs: item.providerAgent ? { [provider]: { agent: item.providerAgent } } : undefined,
892
+ })),
893
+ };
894
+ }
895
+ function resolveExpertCatalogOutput(provider, providerConfig, teamConfig, out) {
896
+ return out ??
897
+ teamConfig.providerCatalogs?.[provider]?.path ??
898
+ providerConfig.expertCatalog?.path ??
899
+ `.zsk/providers/${provider}/experts.yaml`;
900
+ }
901
+ function expertCatalogPathIsConfigured(outputRel, provider, providerConfig, teamConfig) {
902
+ const configured = teamConfig.providerCatalogs?.[provider]?.path ?? providerConfig.expertCatalog?.path;
903
+ if (!configured)
904
+ return outputRel === `.zsk/providers/${provider}/experts.yaml`;
905
+ return toPosix(configured) === toPosix(outputRel);
906
+ }
907
+ function expertImportQualityGates(items, normalizedCount, outputConfigured, dryRun) {
908
+ const blockedCount = items.length - normalizedCount;
909
+ return [
910
+ {
911
+ id: "experts-normalized",
912
+ status: normalizedCount > 0 ? "pass" : "fail",
913
+ detail: normalizedCount > 0 ? `${normalizedCount} expert(s) ready for catalog` : "no valid experts with id, provider agent, and roles",
914
+ },
915
+ {
916
+ id: "blocked-experts",
917
+ status: blockedCount === 0 ? "pass" : "fail",
918
+ detail: blockedCount === 0 ? "all expert records are usable" : `${blockedCount} expert record(s) are missing required fields`,
919
+ },
920
+ {
921
+ id: "dispatch-readable-catalog",
922
+ status: outputConfigured ? "pass" : "fail",
923
+ detail: outputConfigured
924
+ ? "catalog path is configured or matches the dispatch auto-discovery path"
925
+ : "catalog path is not configured in .zsk/team.yaml or .zsk/work.yaml",
926
+ },
927
+ {
928
+ id: "provider-side-effects",
929
+ status: "pass",
930
+ detail: dryRun ? "dry-run only; no local or remote side effects" : "local catalog write only; no provider command or API call was executed",
931
+ },
932
+ ];
933
+ }
934
+ function renderExpertImportPlan(plan) {
935
+ const rows = plan.items.map((item) => [
936
+ item.sourceId ?? "",
937
+ item.id ?? "",
938
+ item.providerAgent ?? "",
939
+ item.roles.join(", "),
940
+ item.stages.join(", "),
941
+ item.modules.join(", "),
942
+ item.subagentTypes.join(", "),
943
+ item.blockedReasons.length === 0 ? "yes" : item.blockedReasons.join(", "),
944
+ ].join(" | "));
945
+ return [
946
+ `# Work Expert Import${plan.dryRun ? " Dry Run" : ""}`,
947
+ "",
948
+ `- Provider: ${plan.provider}`,
949
+ `- Adapter: ${plan.adapter ?? "unknown"}`,
950
+ `- Input: ${plan.inputFile}`,
951
+ `- Output: ${plan.outputFile}`,
952
+ `- Output configured: ${plan.outputConfigured ? "yes" : "no"}`,
953
+ `- Remote side effects: ${plan.remoteSideEffects}`,
954
+ `- Local side effects: ${plan.localSideEffects}`,
955
+ `- Items: ${plan.itemCount}`,
956
+ `- Normalized: ${plan.normalizedCount}`,
957
+ `- Blocked: ${plan.blockedCount}`,
958
+ `- Written: ${plan.writtenCount}`,
959
+ "",
960
+ "## Quality Gates",
961
+ "",
962
+ ...plan.qualityGates.map((gate) => `- ${gate.id}: ${gate.status} - ${gate.detail}`),
963
+ "",
964
+ "source id | id | provider agent | roles | stages | modules | subagent types | ready",
965
+ "--- | --- | --- | --- | --- | --- | --- | ---",
966
+ ...(rows.length > 0 ? rows : [" | | | | | | | no expert records"]),
967
+ "",
968
+ ].join("\n");
969
+ }
970
+ function renderStatusImportPlan(plan) {
971
+ const rows = plan.items.map((item) => [
972
+ item.localId ?? "",
973
+ item.remoteId ?? "",
974
+ item.providerStatus ?? "",
975
+ item.localStatus ?? "",
976
+ item.updatedAt ?? "",
977
+ item.blockedReasons.length === 0 ? "yes" : item.blockedReasons.join(", "),
978
+ renderArtifactImportCell(item),
979
+ ].join(" | "));
980
+ return [
981
+ `# Work Status Import${plan.dryRun ? " Dry Run" : ""}`,
982
+ "",
983
+ `- Provider: ${plan.provider}`,
984
+ `- Adapter: ${plan.adapter ?? "unknown"}`,
985
+ `- Input: ${plan.inputFile}`,
986
+ `- Ledger: ${plan.ledger}`,
987
+ `- Remote side effects: ${plan.remoteSideEffects}`,
988
+ `- Local side effects: ${plan.localSideEffects}`,
989
+ `- Items: ${plan.itemCount}`,
990
+ `- Mapped: ${plan.mappedCount}`,
991
+ `- Blocked: ${plan.blockedCount}`,
992
+ `- Appended: ${plan.appendedCount}`,
993
+ `- Artifact updates: ${plan.artifactUpdateCount}`,
994
+ "",
995
+ "local id | remote id | provider status | local status | updated at | import ready | artifacts",
996
+ "--- | --- | --- | --- | --- | --- | ---",
997
+ ...(rows.length > 0 ? rows : [" | | | | | no items | "]),
998
+ "",
999
+ ].join("\n");
1000
+ }
1001
+ function renderArtifactImportCell(item) {
1002
+ if (item.updatedArtifacts.length > 0)
1003
+ return item.updatedArtifacts.join(", ");
1004
+ if (item.artifactTargets.length > 0)
1005
+ return item.artifactTargets.map((target) => `target:${target}`).join(", ");
1006
+ return item.artifactNotes.join("; ");
1007
+ }
1008
+ function parseTaskWorkItems(input) {
1009
+ const sourceArtifact = projectRelative(input.target, input.taskArtifactPath);
1010
+ const groups = taskGroupSections(input.content);
1011
+ return groups.flatMap((group) => {
1012
+ const metadata = taskGroupMetadata(group.body);
1013
+ const localId = metadata["work item id"] ?? taskMarkerId(group.body);
1014
+ const localStatus = localId ? taskMarkerStatus(group.body, localId) : null;
1015
+ const project = resolveWorkProject(input.projectId, input.moduleId, input.providerConfig);
1016
+ const blockedReasons = [];
1017
+ if (!localId)
1018
+ blockedReasons.push("workItem.id");
1019
+ if (!localStatus)
1020
+ blockedReasons.push("zsk-task-status.status");
1021
+ if (!project)
1022
+ blockedReasons.push("workItem.project");
1023
+ if (!input.moduleId)
1024
+ blockedReasons.push("workItem.module");
1025
+ if (!input.stage)
1026
+ blockedReasons.push("workItem.stage");
1027
+ if (!sourceArtifact)
1028
+ blockedReasons.push("workItem.sourceArtifacts");
1029
+ const item = {
1030
+ localId,
1031
+ title: group.title,
1032
+ itemKind: "task-group",
1033
+ parentId: null,
1034
+ sequence: group.sequence,
1035
+ project,
1036
+ module: input.moduleId,
1037
+ stage: input.stage,
1038
+ localStatus,
1039
+ owner: metadata.owner ?? null,
1040
+ scope: metadata["scope / files"] ?? metadata["scope/files"] ?? metadata.scope ?? null,
1041
+ linkedRefs: metadata["linked fr/ac"] ?? metadata.source ?? null,
1042
+ evidenceHook: metadata["evidence hook"] ?? metadata.evidence ?? null,
1043
+ stopCondition: metadata["stop condition"] ?? null,
1044
+ sourceArtifact,
1045
+ blockedReasons,
1046
+ willAppend: false,
1047
+ linkWillAppend: false,
1048
+ skipReason: null,
1049
+ };
1050
+ const items = [finalizeTaskEmitPlanItem(item, input.existingEvents)];
1051
+ if (blockedReasons.length === 0 && localId && localStatus) {
1052
+ items.push(...taskSubtaskSections(group.body).map((subtask) => {
1053
+ const subtaskMetadata = taskItemMetadata(subtask.body);
1054
+ const subtaskLocalId = subtaskMetadata["work item id"] ?? taskMarkerId(subtask.body) ?? `${localId}-${subtask.sequence.replace(/\./g, "-")}`;
1055
+ const subtaskStatus = taskMarkerStatus(subtask.body, subtaskLocalId) ?? subtaskMetadata.status ?? localStatus;
1056
+ const childBlockedReasons = [];
1057
+ if (!subtaskLocalId)
1058
+ childBlockedReasons.push("workItem.id");
1059
+ if (!subtaskStatus)
1060
+ childBlockedReasons.push("zsk-task-status.status");
1061
+ if (!project)
1062
+ childBlockedReasons.push("workItem.project");
1063
+ if (!input.moduleId)
1064
+ childBlockedReasons.push("workItem.module");
1065
+ if (!input.stage)
1066
+ childBlockedReasons.push("workItem.stage");
1067
+ if (!sourceArtifact)
1068
+ childBlockedReasons.push("workItem.sourceArtifacts");
1069
+ if (!localId)
1070
+ childBlockedReasons.push("workItem.parentId");
1071
+ const child = {
1072
+ localId: subtaskLocalId,
1073
+ title: subtask.title,
1074
+ itemKind: "subtask",
1075
+ parentId: localId,
1076
+ sequence: subtask.sequence,
1077
+ project,
1078
+ module: input.moduleId,
1079
+ stage: input.stage,
1080
+ localStatus: subtaskStatus,
1081
+ owner: subtaskMetadata.owner ?? item.owner,
1082
+ scope: subtaskMetadata["scope / files"] ?? subtaskMetadata["scope/files"] ?? subtaskMetadata.scope ?? item.scope,
1083
+ linkedRefs: subtaskMetadata["linked fr/ac"] ?? subtaskMetadata.source ?? subtaskMetadata.acceptance ?? item.linkedRefs,
1084
+ evidenceHook: subtaskMetadata["evidence hook"] ?? subtaskMetadata.evidence ?? item.evidenceHook,
1085
+ stopCondition: subtaskMetadata["stop condition"] ?? subtaskMetadata.acceptance ?? null,
1086
+ sourceArtifact,
1087
+ blockedReasons: childBlockedReasons,
1088
+ willAppend: false,
1089
+ linkWillAppend: false,
1090
+ skipReason: null,
1091
+ };
1092
+ return finalizeTaskEmitPlanItem(child, input.existingEvents);
1093
+ }));
1094
+ }
1095
+ return items;
1096
+ });
1097
+ }
1098
+ function finalizeTaskEmitPlanItem(item, existingEvents) {
1099
+ if (item.blockedReasons.length > 0)
1100
+ return item;
1101
+ const previous = item.localId ? latestWorkItem(existingEvents, item.localId) : undefined;
1102
+ if (previous && sameTaskWorkItem(previous, item)) {
1103
+ return {
1104
+ ...item,
1105
+ linkWillAppend: item.itemKind === "subtask" && !hasParentChildLink(existingEvents, item),
1106
+ skipReason: "unchanged from latest work item event",
1107
+ };
1108
+ }
1109
+ return {
1110
+ ...item,
1111
+ willAppend: true,
1112
+ linkWillAppend: item.itemKind === "subtask" && !hasParentChildLink(existingEvents, item),
1113
+ };
1114
+ }
1115
+ function taskGroupSections(content) {
1116
+ const lines = content.split(/\r?\n/);
1117
+ const groups = [];
1118
+ let current = null;
1119
+ for (const line of lines) {
1120
+ const match = line.match(/^- \[[ xX]\] (\d+)\.\s+(.+)$/);
1121
+ if (match) {
1122
+ if (current)
1123
+ groups.push({ title: current.title, body: current.lines.join("\n"), sequence: current.sequence });
1124
+ current = { title: match[2]?.trim() ?? "", lines: [line], sequence: match[1]?.trim() ?? "" };
1125
+ continue;
1126
+ }
1127
+ if (current)
1128
+ current.lines.push(line);
1129
+ }
1130
+ if (current)
1131
+ groups.push({ title: current.title, body: current.lines.join("\n"), sequence: current.sequence });
1132
+ return groups;
1133
+ }
1134
+ function taskGroupMetadata(body) {
1135
+ return taskItemMetadata(body, true);
1136
+ }
1137
+ function taskItemMetadata(body, stopAtSubtask = false) {
1138
+ const metadata = {};
1139
+ for (const line of body.split(/\r?\n/)) {
1140
+ if (stopAtSubtask && /^\s+- \[[ xX]\] \d+\.\d+(?:\.\d+)*\s+/.test(line))
1141
+ break;
1142
+ const match = line.match(/^\s*-\s+([^:]+):\s*(.*)$/);
1143
+ if (!match)
1144
+ continue;
1145
+ const key = match[1]?.trim().toLowerCase();
1146
+ if (!key || key.startsWith("["))
1147
+ continue;
1148
+ metadata[key] = match[2]?.trim() ?? "";
1149
+ }
1150
+ return metadata;
1151
+ }
1152
+ function taskSubtaskSections(body) {
1153
+ const lines = body.split(/\r?\n/);
1154
+ const subtasks = [];
1155
+ let current = null;
1156
+ for (const line of lines) {
1157
+ const match = line.match(/^\s+- \[[ xX]\] (\d+\.\d+(?:\.\d+)*)\s+(.+)$/);
1158
+ if (match) {
1159
+ if (current)
1160
+ subtasks.push({ title: current.title, body: current.lines.join("\n"), sequence: current.sequence });
1161
+ current = { title: match[2]?.trim() ?? "", lines: [line], sequence: match[1]?.trim() ?? "" };
1162
+ continue;
1163
+ }
1164
+ if (current)
1165
+ current.lines.push(line);
1166
+ }
1167
+ if (current)
1168
+ subtasks.push({ title: current.title, body: current.lines.join("\n"), sequence: current.sequence });
1169
+ return subtasks;
1170
+ }
1171
+ function taskMarkerId(body) {
1172
+ const match = body.match(/<!--\s*zsk-task-status:start\s+([^\s]+)\s*-->/);
1173
+ return match?.[1]?.trim() ?? null;
1174
+ }
1175
+ function taskMarkerStatus(body, localId) {
1176
+ const marker = body.match(taskStatusMarkerRegex(localId))?.[0] ?? "";
1177
+ const status = marker.match(/^\s*-\s+status:\s*(.+?)\s*$/m)?.[1]?.trim();
1178
+ return status || null;
1179
+ }
1180
+ function sameTaskWorkItem(previous, item) {
1181
+ return previous.id === item.localId &&
1182
+ previous.type === "task" &&
1183
+ previous.title === item.title &&
1184
+ previous.itemKind === item.itemKind &&
1185
+ previous.parentId === item.parentId &&
1186
+ previous.sequence === item.sequence &&
1187
+ idOf(previous.project) === item.project &&
1188
+ idOf(previous.module) === item.module &&
1189
+ previous.stage === item.stage &&
1190
+ previous.status === item.localStatus &&
1191
+ stringsOf(previous.sourceArtifacts).includes(item.sourceArtifact) &&
1192
+ previous.owner === item.owner &&
1193
+ previous.scope === item.scope &&
1194
+ previous.linkedRefs === item.linkedRefs &&
1195
+ previous.evidenceHook === item.evidenceHook &&
1196
+ previous.stopCondition === item.stopCondition;
1197
+ }
1198
+ function hasParentChildLink(events, item) {
1199
+ if (!item.localId || !item.parentId)
1200
+ return false;
1201
+ return events.some((event) => event.event === "work.item.linked" &&
1202
+ event.workItem?.id === item.localId &&
1203
+ event.links?.relation === "parent-child" &&
1204
+ event.links?.parentId === item.parentId &&
1205
+ event.links?.childId === item.localId);
1206
+ }
1207
+ function buildTaskEmitEvents(input) {
1208
+ return input.items.flatMap((item) => {
1209
+ if (!item.localId || !item.localStatus || !item.project || !item.module)
1210
+ return [];
1211
+ const workItem = {
1212
+ id: item.localId,
1213
+ type: "task",
1214
+ title: item.title,
1215
+ project: item.project,
1216
+ module: item.module,
1217
+ stage: item.stage,
1218
+ status: item.localStatus,
1219
+ sourceArtifacts: [item.sourceArtifact],
1220
+ owner: item.owner,
1221
+ scope: item.scope,
1222
+ linkedRefs: item.linkedRefs,
1223
+ evidenceHook: item.evidenceHook,
1224
+ stopCondition: item.stopCondition,
1225
+ itemKind: item.itemKind,
1226
+ parentId: item.parentId,
1227
+ sequence: item.sequence,
1228
+ };
1229
+ const events = [];
1230
+ if (item.willAppend) {
1231
+ events.push({
1232
+ event: "work.item.upserted",
1233
+ version: 1,
1234
+ occurredAt: input.emittedAt,
1235
+ producer: {
1236
+ skill: "task",
1237
+ stage: "task",
1238
+ },
1239
+ workItem,
1240
+ links: {
1241
+ taskArtifact: item.sourceArtifact,
1242
+ linkedRefs: item.linkedRefs,
1243
+ parentId: item.parentId,
1244
+ },
1245
+ evidence: {
1246
+ taskEmitPlan: input.evidencePath,
1247
+ },
1248
+ });
1249
+ }
1250
+ if (item.linkWillAppend && item.itemKind === "subtask" && item.parentId) {
1251
+ events.push({
1252
+ event: "work.item.linked",
1253
+ version: 1,
1254
+ occurredAt: input.emittedAt,
1255
+ producer: {
1256
+ skill: "task",
1257
+ stage: "task",
1258
+ },
1259
+ workItem,
1260
+ links: {
1261
+ relation: "parent-child",
1262
+ parentId: item.parentId,
1263
+ childId: item.localId,
1264
+ taskArtifact: item.sourceArtifact,
1265
+ linkedRefs: item.linkedRefs,
1266
+ },
1267
+ evidence: {
1268
+ taskEmitPlan: input.evidencePath,
1269
+ },
1270
+ });
1271
+ }
1272
+ return events;
1273
+ });
1274
+ }
1275
+ function buildAssignmentEvents(input) {
1276
+ if (input.providerCommandPlan.blockedCount > 0)
1277
+ return [];
1278
+ const latestAssignment = latestAssignmentEvent(input.existingEvents, input.workItem.id);
1279
+ if (latestAssignment && sameAssignmentEvent(latestAssignment, input.assignment, input.provider))
1280
+ return [];
1281
+ const evidencePath = projectRelative(input.target, input.evidencePath);
1282
+ const sourceArtifacts = uniqueStrings([
1283
+ ...stringsOf(input.previous?.sourceArtifacts),
1284
+ evidencePath,
1285
+ ]);
1286
+ const externalRefs = isRecord(input.previous?.externalRefs) ? { ...input.previous.externalRefs } : {};
1287
+ const assignee = input.assignment.providerAgent ?? input.assignment.agent ?? input.assignment.squad ?? null;
1288
+ const providerRefs = {};
1289
+ if (input.assignment.providerAgent) {
1290
+ providerRefs[input.provider] = { agent: input.assignment.providerAgent };
1291
+ }
1292
+ else if (input.assignment.agent) {
1293
+ providerRefs[input.provider] = { agent: input.assignment.agent };
1294
+ }
1295
+ else if (input.assignment.squad) {
1296
+ providerRefs[input.provider] = { squad: input.assignment.squad };
1297
+ }
1298
+ return [{
1299
+ event: "work.assignment.requested",
1300
+ version: 1,
1301
+ occurredAt: input.assignedAt,
1302
+ producer: {
1303
+ skill: "work",
1304
+ stage: "assign",
1305
+ },
1306
+ workItem: {
1307
+ id: input.workItem.id,
1308
+ type: input.previous?.type ?? "task",
1309
+ title: input.previous?.title ?? `Assignment requested for ${input.workItem.id}`,
1310
+ project: input.workItem.project,
1311
+ module: input.workItem.module,
1312
+ stage: input.workItem.stage,
1313
+ status: input.previous?.status ?? "ready",
1314
+ sourceArtifacts,
1315
+ externalRefs,
1316
+ owner: assignee,
1317
+ },
1318
+ assignment: {
1319
+ mode: input.assignment.mode,
1320
+ role: input.assignment.role,
1321
+ agent: input.assignment.agent,
1322
+ squad: input.assignment.squad,
1323
+ providerAgent: input.assignment.providerAgent,
1324
+ assignee,
1325
+ providerRefs,
1326
+ rationale: input.assignment.rationale,
1327
+ requestedAt: input.assignedAt,
1328
+ },
1329
+ links: {
1330
+ provider: input.provider,
1331
+ assignmentPlan: evidencePath,
1332
+ },
1333
+ evidence: {
1334
+ assignmentPlan: evidencePath,
1335
+ },
1336
+ }];
1337
+ }
1338
+ function taskEmitQualityGates(items, appendableCount, dryRun) {
1339
+ const blockedCount = items.filter((item) => item.blockedReasons.length > 0).length;
1340
+ return [
1341
+ {
1342
+ id: "task-marker-schema",
1343
+ status: blockedCount === 0 ? "pass" : "fail",
1344
+ detail: blockedCount === 0 ? "all task groups and subtasks have work item ids and statuses" : `${blockedCount} task item(s) need work item ids or statuses`,
1345
+ },
1346
+ {
1347
+ id: "append-plan",
1348
+ status: appendableCount > 0 || items.length > 0 ? "pass" : "fail",
1349
+ detail: appendableCount > 0
1350
+ ? `${appendableCount} work item event(s) ready to append`
1351
+ : "no changed task work item events to append",
1352
+ },
1353
+ {
1354
+ id: "remote-side-effects",
1355
+ status: "pass",
1356
+ detail: dryRun ? "dry-run only; no local or remote side effects" : "local ledger append only; no provider command or API call was executed",
1357
+ },
1358
+ ];
1359
+ }
1360
+ function renderTaskEmitPlan(plan) {
1361
+ const rows = plan.items.map((item) => [
1362
+ item.localId ?? "",
1363
+ item.itemKind,
1364
+ item.parentId ?? "",
1365
+ item.title,
1366
+ item.project ?? "",
1367
+ item.module ?? "",
1368
+ item.stage,
1369
+ item.localStatus ?? "",
1370
+ item.willAppend || item.linkWillAppend ? "yes" : item.skipReason ?? item.blockedReasons.join(", "),
1371
+ ].join(" | "));
1372
+ return [
1373
+ "# Work Task Emit Plan",
1374
+ "",
1375
+ `- Provider: ${plan.provider}`,
1376
+ `- Adapter: ${plan.adapter ?? "unknown"}`,
1377
+ `- Generated at: ${plan.generatedAt}`,
1378
+ `- Work config: ${plan.workConfig}`,
1379
+ `- Ledger: ${plan.ledger}`,
1380
+ `- Tasks artifact: ${plan.tasksArtifact}`,
1381
+ `- Remote side effects: ${plan.remoteSideEffects}`,
1382
+ `- Local side effects: ${plan.localSideEffects}`,
1383
+ `- Items: ${plan.itemCount}`,
1384
+ `- Appendable: ${plan.appendableCount}`,
1385
+ `- Blocked: ${plan.blockedCount}`,
1386
+ `- Skipped: ${plan.skippedCount}`,
1387
+ `- Appended: ${plan.appendedCount}`,
1388
+ "",
1389
+ "## Quality Gates",
1390
+ "",
1391
+ ...plan.qualityGates.map((gate) => `- ${gate.id}: ${gate.status} - ${gate.detail}`),
1392
+ "",
1393
+ "## Task Work Items",
1394
+ "",
1395
+ "local id | kind | parent id | title | project | module | stage | status | append ready",
1396
+ "--- | --- | --- | --- | --- | --- | --- | --- | ---",
1397
+ ...(rows.length > 0 ? rows : [" | | | | | | | | no task items"]),
1398
+ "",
1399
+ ].join("\n");
1400
+ }
1401
+ function resolveStatusImportLocalSideEffects(dryRun, appendedCount, artifactUpdateCount) {
1402
+ if (dryRun || (appendedCount === 0 && artifactUpdateCount === 0))
1403
+ return "none";
1404
+ if (appendedCount > 0 && artifactUpdateCount > 0)
1405
+ return "append-work-ledger-and-update-artifacts";
1406
+ if (appendedCount > 0)
1407
+ return "append-work-ledger";
1408
+ return "update-artifacts";
1409
+ }
1410
+ async function planLocalArtifactTargets(input) {
1411
+ const references = uniqueStrings(collectIssueArtifactReferences(input.record, input.previous));
1412
+ const targets = [];
1413
+ const notes = [];
1414
+ if (references.length === 0)
1415
+ return { targets, notes: ["no explicit local artifact reference"] };
1416
+ for (const reference of references) {
1417
+ const artifact = artifactFileFromReference(input.target, input.config, reference);
1418
+ if (!artifact) {
1419
+ notes.push(`unsupported artifact reference: ${reference}`);
1420
+ continue;
1421
+ }
1422
+ if (!await canReadFile(artifact.path)) {
1423
+ notes.push(`missing local artifact: ${projectRelative(input.target, artifact.path)}`);
1424
+ continue;
1425
+ }
1426
+ if (artifact.kind === "task") {
1427
+ const content = await readFile(artifact.path, "utf8");
1428
+ if (!input.localId || !hasTaskStatusMarker(content, input.localId)) {
1429
+ notes.push(`task artifact lacks zsk-task-status marker for ${input.localId ?? "unknown"}: ${projectRelative(input.target, artifact.path)}`);
1430
+ continue;
1431
+ }
1432
+ }
1433
+ targets.push(projectRelative(input.target, artifact.path));
1434
+ }
1435
+ return { targets: uniqueStrings(targets), notes };
1436
+ }
1437
+ async function applyStatusImportArtifacts(input) {
1438
+ let updated = 0;
1439
+ let issueUpdated = false;
1440
+ for (const item of input.items) {
1441
+ if (item.blockedReasons.length > 0 || !item.localStatus)
1442
+ continue;
1443
+ for (const artifactTarget of item.artifactTargets) {
1444
+ const artifactFile = resolveInsideProject(input.target, artifactTarget, "local artifact");
1445
+ try {
1446
+ const content = await readFile(artifactFile, "utf8");
1447
+ const note = [
1448
+ `provider ${input.provider} status ${item.providerStatus}`,
1449
+ item.remoteId ? `remote ${item.remoteId}` : "",
1450
+ ].filter(Boolean).join("; ");
1451
+ const issueArtifact = isAllowedIssueArtifact(input.target, input.config, artifactFile);
1452
+ const nextContent = issueArtifact
1453
+ ? updateIssueStatusBlock(content, item.localStatus, note, item.updatedAt ?? input.importedAt)
1454
+ : updateTaskStatusMarker(content, {
1455
+ localId: item.localId,
1456
+ localStatus: item.localStatus,
1457
+ provider: input.provider,
1458
+ providerStatus: item.providerStatus,
1459
+ remoteId: item.remoteId,
1460
+ updatedAt: item.updatedAt ?? input.importedAt,
1461
+ });
1462
+ await writeFile(artifactFile, nextContent, "utf8");
1463
+ item.updatedArtifacts.push(projectRelative(input.target, artifactFile));
1464
+ if (issueArtifact)
1465
+ issueUpdated = true;
1466
+ updated += 1;
1467
+ }
1468
+ catch {
1469
+ item.artifactNotes.push(`failed to update local artifact: ${artifactTarget}`);
1470
+ }
1471
+ }
1472
+ }
1473
+ if (issueUpdated) {
1474
+ await refreshIssueIndexes(resolve(input.target, getWorkspacePath(input.config, "issuesRoot")), resolve(input.target, getWorkspacePath(input.config, "modulesRoot")));
1475
+ }
1476
+ return updated;
1477
+ }
1478
+ function buildStatusImportEvents(input) {
1479
+ return input.items
1480
+ .filter((item) => item.blockedReasons.length === 0 && item.localId && item.localStatus)
1481
+ .map((item) => {
1482
+ const previous = latestWorkItem(input.existingEvents, item.localId);
1483
+ const externalRefs = isRecord(previous?.externalRefs) ? { ...previous.externalRefs } : {};
1484
+ if (item.remoteId)
1485
+ externalRefs[input.provider] = item.remoteId;
1486
+ const sourceArtifacts = uniqueStrings([
1487
+ ...stringsOf(previous?.sourceArtifacts),
1488
+ ...item.artifactTargets,
1489
+ projectRelative(input.target, input.inputFile),
1490
+ ]);
1491
+ return {
1492
+ event: "work.provider_status.imported",
1493
+ version: 1,
1494
+ occurredAt: input.importedAt,
1495
+ producer: {
1496
+ skill: "work",
1497
+ stage: "import-status",
1498
+ },
1499
+ workItem: {
1500
+ id: item.localId,
1501
+ type: previous?.type ?? "task",
1502
+ title: previous?.title ?? `Imported provider status for ${item.localId}`,
1503
+ project: previous?.project ?? "unknown",
1504
+ module: previous?.module ?? "unknown",
1505
+ stage: previous?.stage ?? "import-status",
1506
+ status: item.localStatus,
1507
+ sourceArtifacts,
1508
+ externalRefs,
1509
+ },
1510
+ status: {
1511
+ from: previous?.status,
1512
+ to: item.localStatus,
1513
+ provider: input.provider,
1514
+ providerStatus: item.providerStatus,
1515
+ remoteId: item.remoteId,
1516
+ observedAt: item.updatedAt ?? input.importedAt,
1517
+ importedAt: input.importedAt,
1518
+ },
1519
+ links: {
1520
+ provider: input.provider,
1521
+ remoteId: item.remoteId,
1522
+ inputFile: projectRelative(input.target, input.inputFile),
1523
+ },
1524
+ evidence: {
1525
+ statusImportPlan: projectRelative(input.target, input.evidencePath),
1526
+ updatedArtifacts: item.updatedArtifacts,
1527
+ },
1528
+ };
1529
+ });
1530
+ }
1531
+ function collectIssueArtifactReferences(record, previous) {
1532
+ const references = [];
1533
+ if (record) {
1534
+ for (const key of [
1535
+ "issuePath",
1536
+ "issueFile",
1537
+ "taskPath",
1538
+ "taskFile",
1539
+ "artifact",
1540
+ "artifactPath",
1541
+ "artifactFile",
1542
+ "localArtifact",
1543
+ "sourceArtifact",
1544
+ "sourceArtifacts",
1545
+ "path",
1546
+ "links",
1547
+ ]) {
1548
+ references.push(...stringsOf(record[key]));
1549
+ }
1550
+ }
1551
+ references.push(...stringsOf(previous?.sourceArtifacts));
1552
+ return references;
1553
+ }
1554
+ function artifactFileFromReference(target, config, reference) {
1555
+ const cleaned = cleanLocalArtifactReference(reference);
1556
+ if (!cleaned)
1557
+ return null;
1558
+ const resolved = resolveProjectPath(target, cleaned);
1559
+ if (!isPathInside(target, resolved))
1560
+ return null;
1561
+ if (isAllowedIssueArtifact(target, config, resolved))
1562
+ return { path: resolved, kind: "issue" };
1563
+ if (isAllowedTaskArtifact(target, config, resolved))
1564
+ return { path: resolved, kind: "task" };
1565
+ const issueFile = join(resolved, "issue.md");
1566
+ if (isAllowedIssueArtifact(target, config, issueFile))
1567
+ return { path: issueFile, kind: "issue" };
1568
+ return null;
1569
+ }
1570
+ function cleanLocalArtifactReference(reference) {
1571
+ const withoutFragment = reference.trim().split("#")[0]?.split("?")[0]?.trim();
1572
+ if (!withoutFragment)
1573
+ return null;
1574
+ if (/^[a-z][a-z0-9+.-]*:/i.test(withoutFragment))
1575
+ return null;
1576
+ return withoutFragment;
1577
+ }
1578
+ function isAllowedIssueArtifact(target, config, issueFile) {
1579
+ if (!toPosix(issueFile).endsWith("/issue.md"))
1580
+ return false;
1581
+ const issuesRoot = resolve(target, getWorkspacePath(config, "issuesRoot"));
1582
+ if (isPathInside(issuesRoot, issueFile))
1583
+ return true;
1584
+ const modulesRoot = resolve(target, getWorkspacePath(config, "modulesRoot"));
1585
+ return isPathInside(modulesRoot, issueFile) && toPosix(relative(modulesRoot, issueFile)).includes("/_issues/");
1586
+ }
1587
+ function isAllowedTaskArtifact(target, config, taskFile) {
1588
+ if (!toPosix(taskFile).endsWith("/tasks.md"))
1589
+ return false;
1590
+ const modulesRoot = resolve(target, getWorkspacePath(config, "modulesRoot"));
1591
+ const relativePath = toPosix(relative(modulesRoot, taskFile));
1592
+ return isPathInside(modulesRoot, taskFile) && !relativePath.includes("/_issues/") && !relativePath.includes("/_evidence/");
1593
+ }
1594
+ function hasTaskStatusMarker(content, localId) {
1595
+ return taskStatusMarkerRegex(localId).test(content);
1596
+ }
1597
+ function updateTaskStatusMarker(content, input) {
1598
+ const marker = [
1599
+ `<!-- zsk-task-status:start ${input.localId} -->`,
1600
+ `- status: ${input.localStatus}`,
1601
+ `- provider: ${input.provider}`,
1602
+ `- provider_status: ${input.providerStatus ?? ""}`,
1603
+ `- remote_id: ${input.remoteId ?? ""}`,
1604
+ `- updated_at: ${input.updatedAt}`,
1605
+ `<!-- zsk-task-status:end ${input.localId} -->`,
1606
+ ].join("\n");
1607
+ return content.replace(taskStatusMarkerRegex(input.localId), marker);
1608
+ }
1609
+ function taskStatusMarkerRegex(localId) {
1610
+ const id = escapeRegExp(localId);
1611
+ return new RegExp(`<!--\\s*zsk-task-status:start\\s+${id}\\s*-->[\\s\\S]*?<!--\\s*zsk-task-status:end\\s+${id}\\s*-->`, "m");
1612
+ }
1613
+ function escapeRegExp(value) {
1614
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1615
+ }
1616
+ async function canReadFile(path) {
1617
+ try {
1618
+ await access(path);
1619
+ return true;
1620
+ }
1621
+ catch {
1622
+ return false;
1623
+ }
1624
+ }
1625
+ function latestAssignmentEvent(events, localId) {
1626
+ for (const event of [...events].reverse()) {
1627
+ if (event.workItem?.id === localId &&
1628
+ (event.event === "work.assignment.requested" || event.event === "work.assignment.accepted")) {
1629
+ return event;
1630
+ }
1631
+ }
1632
+ return undefined;
1633
+ }
1634
+ function sameAssignmentEvent(event, assignment, provider) {
1635
+ const previous = event.assignment ?? {};
1636
+ return stringOf(previous.mode) === assignment.mode &&
1637
+ stringOf(previous.role) === assignment.role &&
1638
+ stringOf(previous.agent) === assignment.agent &&
1639
+ stringOf(previous.squad) === assignment.squad &&
1640
+ stringOf(previous.providerAgent) === assignment.providerAgent &&
1641
+ stringOf(previous.rationale) === assignment.rationale &&
1642
+ assignmentProviderRef(previous, provider) === (assignment.providerAgent ?? assignment.agent ?? assignment.squad);
1643
+ }
1644
+ function invertMapping(mapping) {
1645
+ const inverted = {};
1646
+ for (const [local, remote] of Object.entries(mapping)) {
1647
+ if (remote)
1648
+ inverted[remote] = local;
1649
+ }
1650
+ return inverted;
1651
+ }
1652
+ function resolveInsideProject(root, value, label) {
1653
+ const resolved = resolveProjectPath(root, value);
1654
+ if (!isPathInside(root, resolved)) {
1655
+ console.error(pc.red(`✗ ${label} must stay inside the project: ${value}`));
1656
+ process.exit(1);
1657
+ }
1658
+ return resolved;
1659
+ }
1660
+ function providerLooksConfigured(provider) {
1661
+ const workspace = provider.workspace;
1662
+ if (!workspace)
1663
+ return false;
1664
+ return !workspace.includes("<") && !workspace.includes(">");
1665
+ }
1666
+ function idOf(value) {
1667
+ if (typeof value === "string" && value.trim())
1668
+ return value;
1669
+ if (isRecord(value) && typeof value.id === "string" && value.id.trim())
1670
+ return value.id;
1671
+ return null;
1672
+ }
1673
+ function stringOf(value) {
1674
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1675
+ }
1676
+ function firstString(...values) {
1677
+ for (const value of values) {
1678
+ const candidate = stringOf(value);
1679
+ if (candidate)
1680
+ return candidate;
1681
+ }
1682
+ return null;
1683
+ }
1684
+ function numberOf(value) {
1685
+ if (typeof value === "number" && Number.isFinite(value))
1686
+ return value;
1687
+ if (typeof value === "string" && value.trim()) {
1688
+ const parsed = Number(value);
1689
+ return Number.isFinite(parsed) ? parsed : null;
1690
+ }
1691
+ return null;
1692
+ }
1693
+ function stringListOf(value) {
1694
+ if (typeof value === "string" && value.trim()) {
1695
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
1696
+ }
1697
+ if (Array.isArray(value))
1698
+ return value.flatMap(stringListOf);
1699
+ if (!isRecord(value))
1700
+ return [];
1701
+ return [
1702
+ ...stringListOf(value.id),
1703
+ ...stringListOf(value.name),
1704
+ ...stringListOf(value.slug),
1705
+ ...stringListOf(value.key),
1706
+ ];
1707
+ }
1708
+ function normalizeExpertId(value) {
1709
+ return value
1710
+ .trim()
1711
+ .toLowerCase()
1712
+ .replace(/[^a-z0-9._-]+/g, "-")
1713
+ .replace(/^-+|-+$/g, "");
1714
+ }
1715
+ function stringsOf(value) {
1716
+ if (typeof value === "string" && value.trim())
1717
+ return [value.trim()];
1718
+ if (Array.isArray(value))
1719
+ return value.flatMap(stringsOf);
1720
+ if (!isRecord(value))
1721
+ return [];
1722
+ const strings = [];
1723
+ for (const key of [
1724
+ "path",
1725
+ "file",
1726
+ "issuePath",
1727
+ "issueFile",
1728
+ "taskPath",
1729
+ "taskFile",
1730
+ "artifactPath",
1731
+ "artifactFile",
1732
+ "localArtifact",
1733
+ "sourceArtifact",
1734
+ "sourceArtifacts",
1735
+ ]) {
1736
+ strings.push(...stringsOf(value[key]));
1737
+ }
1738
+ return strings;
1739
+ }
1740
+ function uniqueStrings(values) {
1741
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
1742
+ }
1743
+ function compactRecord(value) {
1744
+ const result = {};
1745
+ for (const [key, item] of Object.entries(value)) {
1746
+ if (item === null || item === undefined)
1747
+ continue;
1748
+ if (Array.isArray(item) && item.length === 0)
1749
+ continue;
1750
+ if (isRecord(item) && Object.keys(item).length === 0)
1751
+ continue;
1752
+ result[key] = item;
1753
+ }
1754
+ return result;
1755
+ }
1756
+ function projectRelative(root, value) {
1757
+ const rel = relative(root, value);
1758
+ return rel && !rel.startsWith("..") ? rel : value;
1759
+ }
1760
+ function toPosix(value) {
1761
+ return value.split(/[\\/]+/).join("/");
1762
+ }
1763
+ function isRecord(value) {
1764
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1765
+ }
1766
+ function runId(iso) {
1767
+ return iso.replace(/[:.]/g, "-");
1768
+ }
1769
+ //# sourceMappingURL=work.js.map