akm-cli 0.9.2-alpha.3 → 0.9.2-alpha.5

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 (151) hide show
  1. package/CHANGELOG.md +664 -0
  2. package/STABILITY.md +23 -5
  3. package/dist/assets/hints/cli-hints-full.md +12 -7
  4. package/dist/assets/tasks/core/extract.yml +3 -5
  5. package/dist/assets/tasks/core/improve.yml +3 -5
  6. package/dist/assets/tasks/core/index-refresh.yml +3 -5
  7. package/dist/assets/tasks/core/sync.yml +3 -5
  8. package/dist/assets/tasks/core/version-check.yml +3 -5
  9. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +3 -5
  10. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -6
  11. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +3 -5
  12. package/dist/assets/tasks/improve/akm-improve-frequent.yml +3 -5
  13. package/dist/assets/tasks/improve/akm-improve-nightly.yml +3 -5
  14. package/dist/cli/unknown-flags.js +12 -1
  15. package/dist/cli.js +8 -1
  16. package/dist/commands/command/command-execution.js +23 -2
  17. package/dist/commands/health/improve-metrics.js +38 -0
  18. package/dist/commands/health/plugin-staleness.js +219 -0
  19. package/dist/commands/health/type-directory-check.js +167 -0
  20. package/dist/commands/health/windows.js +8 -4
  21. package/dist/commands/health.js +71 -9
  22. package/dist/commands/improve/extract.js +22 -1
  23. package/dist/commands/lint/index.js +1 -1
  24. package/dist/commands/migrate-cli.js +130 -24
  25. package/dist/commands/proposal/validators/proposal-validators.js +7 -2
  26. package/dist/commands/read/remember-cli.js +6 -1
  27. package/dist/commands/remember.js +70 -0
  28. package/dist/commands/tasks/explain.js +304 -0
  29. package/dist/commands/tasks/tasks-cli.js +185 -3
  30. package/dist/commands/tasks/tasks.js +233 -45
  31. package/dist/commands/workflow/plan.js +159 -0
  32. package/dist/commands/workflow-cli.js +94 -2
  33. package/dist/core/activation-policy.js +2 -12
  34. package/dist/core/adapter/adapters/akm-lint.js +7 -4
  35. package/dist/core/adapter/adapters/akm-metadata.js +26 -14
  36. package/dist/core/adapter/adapters/akm-task-adapter.js +13 -10
  37. package/dist/core/errors.js +45 -0
  38. package/dist/core/json-schema.js +15 -5
  39. package/dist/core/state/migrations.js +57 -0
  40. package/dist/core/state-db.js +16 -14
  41. package/dist/core/subprocess.js +47 -13
  42. package/dist/execution/guarded-source.js +44 -0
  43. package/dist/execution/input-contract.js +250 -0
  44. package/dist/execution/target-ref.js +63 -0
  45. package/dist/indexer/usage/usage-events.js +14 -3
  46. package/dist/integrations/agent/execution-lowering.js +12 -1
  47. package/dist/integrations/harnesses/claude/session-log.js +85 -25
  48. package/dist/integrations/session-logs/pre-filter.js +152 -2
  49. package/dist/output/shapes/passthrough.js +2 -0
  50. package/dist/output/text/helpers.js +1 -1
  51. package/dist/output/text/migrate.js +12 -3
  52. package/dist/output/text/workflow-format.js +192 -10
  53. package/dist/output/text/workflow.js +2 -1
  54. package/dist/registry/semver.js +4 -0
  55. package/dist/runtime.js +1 -0
  56. package/dist/scripts/akm-migrate-node.js +11838 -10118
  57. package/dist/scripts/akm-migrate.js +11828 -10117
  58. package/dist/setup/steps/tasks.js +34 -17
  59. package/dist/storage/repositories/task-history-repository.js +5 -1
  60. package/dist/storage/repositories/workflow-runs-repository.js +144 -6
  61. package/dist/tasks/backends/launchd.js +31 -84
  62. package/dist/tasks/embedded.js +13 -7
  63. package/dist/tasks/model/invocation.js +4 -0
  64. package/dist/tasks/prepare/prepare-script-target.js +9 -0
  65. package/dist/tasks/prepare/prepare-support.js +154 -0
  66. package/dist/tasks/prepare/prepare.js +117 -0
  67. package/dist/tasks/prepare/prepared-execution.js +4 -0
  68. package/dist/tasks/prepare/script-capture.js +80 -0
  69. package/dist/tasks/run/attempt-lifecycle.js +165 -0
  70. package/dist/tasks/run/load-task.js +117 -0
  71. package/dist/tasks/run/provenance.js +20 -0
  72. package/dist/tasks/run/run-command-task.js +92 -0
  73. package/dist/tasks/run/run-native-task.js +222 -0
  74. package/dist/tasks/run/run-task.js +99 -0
  75. package/dist/tasks/run/run-workflow-task.js +222 -0
  76. package/dist/tasks/run/task-history.js +134 -0
  77. package/dist/tasks/run/task-log.js +179 -0
  78. package/dist/tasks/run/task-result.js +19 -0
  79. package/dist/tasks/scheduler-binding.js +66 -2
  80. package/dist/tasks/scheduler-invocation.js +63 -3
  81. package/dist/tasks/scheduler-sync.js +55 -12
  82. package/dist/tasks/source/bounded-document.js +455 -0
  83. package/dist/tasks/source/parse-task-source.js +59 -0
  84. package/dist/tasks/source/project-v4.js +62 -0
  85. package/dist/tasks/source/task-input-diagnostics.js +36 -0
  86. package/dist/tasks/source/task-source-v4.js +626 -0
  87. package/dist/tasks/source-v3.js +10 -733
  88. package/dist/tasks/task-run-reserved-flags.js +79 -0
  89. package/dist/workflows/authoring/authoring.js +17 -8
  90. package/dist/workflows/exec/child-invocation.js +34 -0
  91. package/dist/workflows/exec/child-workflow.js +370 -0
  92. package/dist/workflows/exec/exec-unit.js +50 -170
  93. package/dist/workflows/exec/frozen-judge.js +19 -2
  94. package/dist/workflows/exec/native-executor.js +49 -27
  95. package/dist/workflows/exec/param-secrets.js +12 -0
  96. package/dist/workflows/exec/run-workflow.js +48 -59
  97. package/dist/workflows/exec/step-work.js +222 -80
  98. package/dist/workflows/exec/unit-dispatch.js +72 -0
  99. package/dist/workflows/freeze/child-output-references.js +94 -0
  100. package/dist/workflows/freeze/environment.js +174 -0
  101. package/dist/workflows/freeze/identity.js +22 -0
  102. package/dist/workflows/freeze/resolve-steps.js +78 -0
  103. package/dist/workflows/freeze/source-freeze.js +57 -0
  104. package/dist/workflows/freeze/step-values.js +68 -0
  105. package/dist/workflows/freeze/targets/child-workflow.js +206 -0
  106. package/dist/workflows/freeze/targets/command.js +81 -0
  107. package/dist/workflows/freeze/targets/script.js +57 -0
  108. package/dist/workflows/freeze/targets/shell.js +31 -0
  109. package/dist/workflows/freeze/targets/task.js +179 -0
  110. package/dist/workflows/freeze/task-bindings.js +180 -0
  111. package/dist/workflows/ir/compile.js +59 -11
  112. package/dist/workflows/ir/environment-v4.js +3 -3
  113. package/dist/workflows/ir/freeze-v4.js +41 -7
  114. package/dist/workflows/ir/params.js +58 -131
  115. package/dist/workflows/ir/plan-hash.js +3 -3
  116. package/dist/workflows/ir/schema-v4.js +246 -17
  117. package/dist/workflows/parser.js +74 -2
  118. package/dist/workflows/program/schema.js +5 -2
  119. package/dist/workflows/resource-limits.js +20 -0
  120. package/dist/workflows/runtime/plan-classifier.js +19 -5
  121. package/dist/workflows/runtime/run-outputs.js +103 -0
  122. package/dist/workflows/runtime/runs.js +114 -9
  123. package/dist/workflows/runtime/workflow-asset-loader.js +14 -6
  124. package/dist/workflows/source-files.js +5 -5
  125. package/dist/workflows/source-ir/compare.js +17 -0
  126. package/dist/workflows/source-ir/compile.js +7 -3
  127. package/dist/workflows/source-ir/github-yaml.js +64 -17
  128. package/dist/workflows/source-ir/schema.js +69 -21
  129. package/dist/workflows/source-ir/semantics.js +7 -25
  130. package/dist/workflows/source-ir/triggers.js +79 -0
  131. package/dist/workflows/source-ir/uses.js +33 -7
  132. package/docs/migration/README.md +1 -1
  133. package/docs/migration/release-notes/0.9.2.md +87 -11
  134. package/docs/migration/release-notes/README.md +2 -2
  135. package/docs/migration/v0.8-to-v0.9.md +9 -7
  136. package/docs/migration/v0.9.0-troubleshooting.md +14 -7
  137. package/docs/migration/v0.9.1-to-v0.9.2.md +598 -49
  138. package/docs/reference/README.md +1 -1
  139. package/docs/reference/cli.md +140 -46
  140. package/docs/reference/configuration.md +3 -3
  141. package/docs/reference/supported-formats.md +9 -5
  142. package/docs/reference/tasks.md +338 -75
  143. package/docs/reference/workflow-schema.md +281 -8
  144. package/docs/reference/workflows.md +57 -7
  145. package/package.json +1 -1
  146. package/schemas/akm-task.json +173 -118
  147. package/schemas/akm-workflow.json +28 -0
  148. package/dist/tasks/runner.js +0 -941
  149. package/dist/tasks/runtime-v3.js +0 -281
  150. package/dist/workflows/ir/source-freeze-v4.js +0 -506
  151. package/dist/workflows/source-ir/ordering.js +0 -38
@@ -90,12 +90,155 @@ function classifyEvent(event, akmReadOnlyOps, maxLen) {
90
90
  }
91
91
  return { keep: true, event, truncated: false };
92
92
  }
93
+ /**
94
+ * A parent-side `<task-notification>` event, as Claude Code writes it into a
95
+ * session's own transcript: a `role: "user"` event whose text is (or wraps)
96
+ * `<task-notification>...<task-id>ID</task-id>...<result>TEXT</result>...</task-notification>`.
97
+ * Matched on the tags themselves (not a dedicated field) because
98
+ * {@link SessionEvent} carries no structural provenance beyond `text`/`role`/
99
+ * `filePath` — the same constraint #830 (subagent provenance) worked within.
100
+ */
101
+ const TASK_NOTIFICATION_RE = /<task-notification>[\s\S]*<\/task-notification>/;
102
+ const TASK_ID_RE = /<task-id>([^<]+)<\/task-id>/;
103
+ const RESULT_RE = /<result>([\s\S]*)<\/result>/;
104
+ const SUMMARY_RE = /<summary>([^<]*)<\/summary>/;
105
+ /** Claude Code's own `<summary>` phrasing for a finished agent: `Agent "<description>" finished`. */
106
+ const AGENT_SUMMARY_DESCRIPTION_RE = /^Agent "(.*)" finished$/;
107
+ /** Provenance {@link subagentProvenance} stamps on every folded subagent event; stripped before comparison. */
108
+ const PROVENANCE_PREFIX_RE = /^\[subagent:[^\]]*\][^\n]*\n/;
109
+ /** Claude Code's own agentId file naming: `<...>/subagents/<...>agent-<agentId>.jsonl`. */
110
+ const SUBAGENT_FILEPATH_RE = /agent-([^/\\]+?)\.jsonl$/;
111
+ /** Dice (bigram) similarity at/above this counts as "the same content" (#839). */
112
+ const DEDUPE_SIMILARITY_THRESHOLD = 0.9;
113
+ /** A handful of named-entity decodes — enough for what Claude Code escapes when it wraps `<result>` text in XML. */
114
+ function decodeXmlEntities(text) {
115
+ return text
116
+ .replace(/&lt;/g, "<")
117
+ .replace(/&gt;/g, ">")
118
+ .replace(/&quot;/g, '"')
119
+ .replace(/&#39;/g, "'")
120
+ .replace(/&amp;/g, "&");
121
+ }
122
+ /** Sørensen–Dice coefficient over character bigrams — a cheap, symmetric textual-overlap measure. */
123
+ function diceSimilarity(a, b) {
124
+ if (a.length < 2 || b.length < 2)
125
+ return a === b ? 1 : 0;
126
+ const bigrams = (s) => {
127
+ const counts = new Map();
128
+ for (let i = 0; i < s.length - 1; i++) {
129
+ const bg = s.slice(i, i + 2);
130
+ counts.set(bg, (counts.get(bg) ?? 0) + 1);
131
+ }
132
+ return counts;
133
+ };
134
+ const bigramsA = bigrams(a);
135
+ const bigramsB = bigrams(b);
136
+ let intersection = 0;
137
+ let totalA = 0;
138
+ let totalB = 0;
139
+ for (const count of bigramsA.values())
140
+ totalA += count;
141
+ for (const count of bigramsB.values())
142
+ totalB += count;
143
+ for (const [bg, count] of bigramsA) {
144
+ const other = bigramsB.get(bg);
145
+ if (other)
146
+ intersection += Math.min(count, other);
147
+ }
148
+ return totalA + totalB === 0 ? 1 : (2 * intersection) / (totalA + totalB);
149
+ }
150
+ /**
151
+ * Stub out a parent's `<task-notification>` event when its `<result>` is a
152
+ * near-duplicate of a subagent transcript's own event that ALSO survived
153
+ * into this same kept set (#839).
154
+ *
155
+ * After #830 folds a session's subagent transcripts into its event stream,
156
+ * a completed subagent's report can appear twice: once as the subagent's own
157
+ * folded final event, once as the parent's `<task-notification>` record of
158
+ * that same call — the notification wraps the subagent's own text almost
159
+ * verbatim (Claude Code XML-escapes `<`/`>`/`&`/quotes in the `<result>`
160
+ * body, which {@link decodeXmlEntities} reverses before comparing). Direction
161
+ * is owner-decided (#839): drop the parent's copy, keep the subagent's
162
+ * original — the inverse was evaluated and rejected in #836 because some
163
+ * subagent transcripts consist ONLY of their terminal event, so dropping it
164
+ * would destroy the harvesting #830 added.
165
+ *
166
+ * **Runs on `kept` — the FINAL post-budget list — not the raw stream**, and
167
+ * only stubs a notification when a matching subagent event is ALSO present
168
+ * in that same `kept` list. This is required, not incidental: the recency-
169
+ * biased budget already evicts one side of most raw duplicate pairs before
170
+ * dedupe would matter (#840's design doc measured zero pairs where both
171
+ * copies reached the pre-dedupe prompt across four real sessions), and any
172
+ * future prompt-composition design that stops including subagent-origin
173
+ * events in the prompt at all (#840's recommended "harvest-without-
174
+ * prompting hybrid") makes the parent's `<task-notification>` the ONLY
175
+ * surviving trace of that delegated work. An unconditional raw-stream stub
176
+ * would delete that sole copy the moment the subagent's own event is absent
177
+ * for ANY reason — evicted by budget today, or never present by design
178
+ * tomorrow. Scoping to "both sides survived into the same kept set" makes
179
+ * this dedupe a no-op whenever there is only one copy left to dedupe
180
+ * against, which is exactly the case where deleting it would be a bug, not
181
+ * a fix.
182
+ *
183
+ * Matching is scoped by `<task-id>` (which is the subagent's agentId) to the
184
+ * SPECIFIC subagent transcript it names, via the `agent-<agentId>.jsonl`
185
+ * filename #830's folding already stamps onto every folded event's
186
+ * `filePath` — then requires the decoded `<result>` to be a near-duplicate
187
+ * (Dice similarity ≥ {@link DEDUPE_SIMILARITY_THRESHOLD}) of that subagent's
188
+ * text, not merely a same-agent match. This matters because a task-notification
189
+ * fires every time an agent stops (Claude Code's own note in the event: "the
190
+ * same task-id may notify more than once") — an EARLIER notification for a
191
+ * resumed agent can carry a genuinely different (intermediate) result that
192
+ * must NOT be stubbed just because the ids line up.
193
+ *
194
+ * The event is kept (not dropped) so event counts/timestamps stay stable and
195
+ * the parent's narrative — *why* it delegated — survives as a short stub:
196
+ * `[subagent <agentId> completed: <description>]`.
197
+ */
198
+ function dedupeTaskNotifications(kept) {
199
+ // Index the KEPT subagent events by the agentId embedded in their
200
+ // transcript's filename, so a notification's <task-id> narrows the
201
+ // comparison to the ONE subagent it reports on — and so an agentId with no
202
+ // surviving event here means "nothing to dedupe against", not "assume it
203
+ // exists upstream".
204
+ const byAgentId = new Map();
205
+ for (const event of kept) {
206
+ const agentId = event.filePath?.match(SUBAGENT_FILEPATH_RE)?.[1];
207
+ if (!agentId)
208
+ continue;
209
+ const list = byAgentId.get(agentId);
210
+ if (list)
211
+ list.push(event);
212
+ else
213
+ byAgentId.set(agentId, [event]);
214
+ }
215
+ if (byAgentId.size === 0)
216
+ return kept; // no folded subagent survived the budget — nothing to dedupe
217
+ return kept.map((event) => {
218
+ if (event.role !== "user" || !TASK_NOTIFICATION_RE.test(event.text))
219
+ return event;
220
+ const taskId = event.text.match(TASK_ID_RE)?.[1];
221
+ const resultRaw = event.text.match(RESULT_RE)?.[1];
222
+ if (!taskId || !resultRaw)
223
+ return event; // no <result> (e.g. a background-command notification) — nothing to compare
224
+ const candidates = byAgentId.get(taskId);
225
+ if (!candidates || candidates.length === 0)
226
+ return event; // that subagent's own event didn't survive into this kept set
227
+ const decodedResult = decodeXmlEntities(resultRaw);
228
+ const isDuplicate = candidates.some((c) => diceSimilarity(decodedResult, c.text.replace(PROVENANCE_PREFIX_RE, "")) >= DEDUPE_SIMILARITY_THRESHOLD);
229
+ if (!isDuplicate)
230
+ return event;
231
+ const summary = event.text.match(SUMMARY_RE)?.[1]?.trim();
232
+ const description = (summary && (summary.match(AGENT_SUMMARY_DESCRIPTION_RE)?.[1] ?? summary)) || "completed";
233
+ return { ...event, text: `[subagent ${taskId} completed: ${description}]` };
234
+ });
235
+ }
93
236
  export function preFilterSession(data, options = {}) {
94
237
  const akmReadOnlyOps = options.akmReadOnlyOps ?? DEFAULT_AKM_READONLY_OPS;
95
238
  const maxLen = options.maxEventTextLength ?? DEFAULT_MAX_EVENT_LENGTH;
96
239
  const maxTotalChars = options.maxTotalChars ?? DEFAULT_MAX_TOTAL_CHARS;
97
240
  const droppedByRule = {};
98
- const kept = [];
241
+ let kept = [];
99
242
  let truncatedCount = 0;
100
243
  const candidates = [];
101
244
  for (const event of data.events) {
@@ -138,6 +281,13 @@ export function preFilterSession(data, options = {}) {
138
281
  if (c.truncated)
139
282
  truncatedCount += 1;
140
283
  }
284
+ // Post-pass (#839): dedupe a task-notification against a subagent event
285
+ // ONLY when both survived into this exact kept set — see
286
+ // dedupeTaskNotifications's doc for why that scoping is required. Recompute
287
+ // totalChars afterward since stubbing can only shrink kept text, never move
288
+ // anything across the budget boundary already decided above.
289
+ kept = dedupeTaskNotifications(kept);
290
+ const finalTotalChars = kept.reduce((sum, e) => sum + e.text.length, 0);
141
291
  return {
142
292
  events: kept,
143
293
  stats: {
@@ -145,7 +295,7 @@ export function preFilterSession(data, options = {}) {
145
295
  outputCount: kept.length,
146
296
  droppedByRule,
147
297
  truncatedCount,
148
- totalChars,
298
+ totalChars: finalTotalChars,
149
299
  budgetDroppedCount,
150
300
  },
151
301
  };
@@ -53,6 +53,7 @@ const PASSTHROUGH_COMMANDS = [
53
53
  "sync",
54
54
  "task-add",
55
55
  "task-doctor",
56
+ "task-explain",
56
57
  "task-history",
57
58
  "task-run",
58
59
  "task-sync",
@@ -61,6 +62,7 @@ const PASSTHROUGH_COMMANDS = [
61
62
  "workflow-abandon",
62
63
  "workflow-create",
63
64
  "workflow-list",
65
+ "workflow-plan",
64
66
  "workflow-resume",
65
67
  "workflow-run",
66
68
  "workflow-status",
@@ -20,4 +20,4 @@ export { formatHealthPlain } from "./health-format.js";
20
20
  export { formatLintPlain } from "./lint-format.js";
21
21
  export { formatGateDecisionSummary, formatProposalAcceptPlain, formatProposalDiffPlain, formatProposalDrainPlain, formatProposalListPlain, formatProposalProducerPlain, formatProposalRejectPlain, formatProposalShowPlain, } from "./proposal-format.js";
22
22
  export { formatShowPlain } from "./show-format.js";
23
- export { formatWorkflowCreatePlain, formatWorkflowListPlain, formatWorkflowResumePlain, formatWorkflowRunPlain, formatWorkflowStatusPlain, } from "./workflow-format.js";
23
+ export { formatWorkflowCreatePlain, formatWorkflowListPlain, formatWorkflowPlanPlain, formatWorkflowResumePlain, formatWorkflowRunPlain, formatWorkflowStatusPlain, } from "./workflow-format.js";
@@ -20,16 +20,25 @@ export function formatMigratePlain(result) {
20
20
  const lines = [`${planGlyph(plan.status)} ${plan.status}`];
21
21
  if (plan.taskV3Migration) {
22
22
  const tasks = plan.taskV3Migration;
23
- lines.push(` tasks: ${tasks.changed} change, ${tasks.skipped} current, ${tasks.blocked} blocked`);
23
+ lines.push(` task-v2->v3: ${tasks.changed} change, ${tasks.skipped} current, ${tasks.blocked} blocked`);
24
+ }
25
+ if (plan.taskV4Migration) {
26
+ const tasks = plan.taskV4Migration;
27
+ lines.push(` task-v3->v4: ${tasks.changed} change, ${tasks.skipped} current, ${tasks.blocked} blocked`);
24
28
  }
25
29
  if (plan.blockers?.length) {
26
30
  lines.push("", "blockers:", ...plan.blockers.map((blocker) => ` - ${blocker}`));
27
31
  }
28
32
  if (plan.backupPath) {
29
- lines.push("", `backup: ${plan.backupPath}`);
33
+ lines.push("", `backup (v2->v3): ${plan.backupPath}`);
30
34
  }
31
35
  if (plan.applied !== undefined)
32
- lines.push(`applied: ${plan.applied}`);
36
+ lines.push(`applied (v2->v3): ${plan.applied}`);
37
+ if (plan.taskV4BackupPath) {
38
+ lines.push(`backup (v3->v4): ${plan.taskV4BackupPath}`);
39
+ }
40
+ if (plan.taskV4Applied !== undefined)
41
+ lines.push(`applied (v3->v4): ${plan.taskV4Applied}`);
33
42
  return lines.join("\n");
34
43
  }
35
44
  export const migrateFormatters = [
@@ -27,6 +27,35 @@ export function formatWorkflowListPlain(result) {
27
27
  })
28
28
  .join("\n");
29
29
  }
30
+ /** §4.5's glyph table (P3b) — drawn from the repo's existing vocabulary (status-list.ts's ✗/✓, proposal-format.ts's →). */
31
+ const CHILD_STATUS_GLYPHS = {
32
+ completed: "✓",
33
+ active: "→",
34
+ blocked: "⚠",
35
+ failed: "✗",
36
+ };
37
+ /**
38
+ * Render one `children:` tree node (P3b, spec §4.5) plus its own children,
39
+ * recursively, indenting two spaces per level. Order is SPAWN order (the
40
+ * envelope's own array order) — the tree is structural, never severity-sorted.
41
+ */
42
+ function renderChildNode(child, depth, lines) {
43
+ const indent = " ".repeat(depth + 1);
44
+ const runId = typeof child.runId === "string" ? child.runId : "unknown";
45
+ const ref = typeof child.workflowRef === "string" ? child.workflowRef : "unknown";
46
+ const status = typeof child.status === "string" ? child.status : "unknown";
47
+ const glyph = CHILD_STATUS_GLYPHS[status] ?? "?";
48
+ const stepId = typeof child.stepId === "string" ? ` (step "${child.stepId}")` : "";
49
+ lines.push(`${indent}- ${glyph} ${runId} ${ref} [${status}]${stepId}`);
50
+ const resume = typeof child.resume === "object" && child.resume !== null ? child.resume : undefined;
51
+ if (resume) {
52
+ lines.push(`${indent} resume: ${typeof resume.command === "string" ? resume.command : ""}`);
53
+ lines.push(`${indent} then: ${typeof resume.then === "string" ? resume.then : ""}`);
54
+ }
55
+ const nested = Array.isArray(child.children) ? child.children : [];
56
+ for (const grandchild of nested)
57
+ renderChildNode(grandchild, depth + 1, lines);
58
+ }
30
59
  export function formatWorkflowStatusPlain(result) {
31
60
  const run = typeof result.run === "object" && result.run !== null ? result.run : undefined;
32
61
  const workflow = typeof result.workflow === "object" && result.workflow !== null
@@ -55,6 +84,16 @@ export function formatWorkflowStatusPlain(result) {
55
84
  }
56
85
  }
57
86
  }
87
+ // The parent-child status tree (P3b, spec §4.5): renders ONLY when
88
+ // `children` is a non-empty array, immediately after `steps:` and before
89
+ // `units:`. Absent for a childless run — byte-identical to pre-P3b text
90
+ // (row B-33, PRESERVE).
91
+ const children = Array.isArray(result.children) ? result.children : undefined;
92
+ if (children && children.length > 0) {
93
+ lines.push("children:");
94
+ for (const child of children)
95
+ renderChildNode(child, 0, lines);
96
+ }
58
97
  // `workflow status --units` (#22): the honest per-unit diagnostic surface —
59
98
  // failure_reason plus any journaled result/error text. Diagnostics only; the
60
99
  // deterministic step evidence above is unaffected.
@@ -108,6 +147,27 @@ function formatWorkflowCheckinLine(result) {
108
147
  return null;
109
148
  return checkin.directive.trim();
110
149
  }
150
+ /**
151
+ * One `! lowering[<severity>] <code> (<adapter>[; <field>]): <message>` line
152
+ * from a `{code, severity, adapter, field?, message}` lowering notice, or
153
+ * `null` when `value` does not carry that shape. Shared by
154
+ * `formatWorkflowRunPlain` (dispatch-time notices) and `formatWorkflowPlanPlain`
155
+ * (freeze-time notices, P3b §4.6, B-57) — ONE projection, rendered the same
156
+ * way regardless of which invocation computed the notice.
157
+ */
158
+ function renderLoweringNoticeLine(value) {
159
+ if (typeof value !== "object" || value === null)
160
+ return null;
161
+ const notice = value;
162
+ if (typeof notice.code !== "string" ||
163
+ typeof notice.severity !== "string" ||
164
+ typeof notice.adapter !== "string" ||
165
+ typeof notice.message !== "string") {
166
+ return null;
167
+ }
168
+ const field = typeof notice.field === "string" && notice.field ? `; ${notice.field}` : "";
169
+ return `! lowering[${notice.severity}] ${notice.code} (${notice.adapter}${field}): ${notice.message}`;
170
+ }
111
171
  export function formatWorkflowRunPlain(result) {
112
172
  const run = typeof result.run === "object" && result.run !== null ? result.run : undefined;
113
173
  if (!run)
@@ -123,16 +183,9 @@ export function formatWorkflowRunPlain(result) {
123
183
  // default text output has the same observability as JSON without widening a
124
184
  // durable workflow schema.
125
185
  for (const value of Array.isArray(result.notices) ? result.notices : []) {
126
- if (typeof value !== "object" || value === null)
127
- continue;
128
- const notice = value;
129
- if (typeof notice.code !== "string" ||
130
- typeof notice.severity !== "string" ||
131
- typeof notice.adapter !== "string" ||
132
- typeof notice.message !== "string")
133
- continue;
134
- const field = typeof notice.field === "string" && notice.field ? `; ${notice.field}` : "";
135
- lines.push(`! lowering[${notice.severity}] ${notice.code} (${notice.adapter}${field}): ${notice.message}`);
186
+ const line = renderLoweringNoticeLine(value);
187
+ if (line)
188
+ lines.push(line);
136
189
  }
137
190
  const executed = Array.isArray(result.executed) ? result.executed : [];
138
191
  if (executed.length === 0) {
@@ -180,3 +233,132 @@ export function formatWorkflowCreatePlain(r) {
180
233
  export function formatWorkflowResumePlain(r) {
181
234
  return formatWorkflowStatusPlain(r) ?? `Resumed workflow run ${String(r.id ?? r.runId ?? "?")}`;
182
235
  }
236
+ /**
237
+ * A step's `inputBindings[]` (P3b §4.6's `with:` line) — a literal shows its
238
+ * value, a reference shows only its unresolved `from` (never a resolved
239
+ * value; B-52/B-53's closed print list explicitly allows a literal *task/
240
+ * child* input binding's `.value`, unlike a literal **environment** binding's).
241
+ */
242
+ function formatInputBindingsList(bindings) {
243
+ return bindings
244
+ .map((binding) => {
245
+ const name = String(binding.name ?? "");
246
+ return binding.kind === "literal"
247
+ ? `${name}=${JSON.stringify(binding.value)} (literal)`
248
+ : `${name} <- ${String(binding.from ?? "")} (reference)`;
249
+ })
250
+ .join(", ");
251
+ }
252
+ /**
253
+ * One `akm workflow plan` step line (P3b, spec §4.6), plus its child-workflow
254
+ * expansion's own steps, recursively, indented one level per composition
255
+ * boundary. A child step also carries the child's `planHash` on the step
256
+ * line itself, and (when present) a `with:` line for the step's own
257
+ * `inputBindings[]` and an `exports:` line for the child's declared output
258
+ * names — §4.6's worked example.
259
+ */
260
+ function renderPlanStepLines(step, ordinal, lines, prefix = " ") {
261
+ const stepId = typeof step.stepId === "string" ? step.stepId : "unknown";
262
+ const targetKind = typeof step.targetKind === "string" ? `[${step.targetKind}]` : "";
263
+ const expansion = typeof step.expansion === "object" && step.expansion !== null ? step.expansion : {};
264
+ const via = typeof expansion.via === "string" ? expansion.via : "direct";
265
+ const childPlanHash = typeof expansion.childPlanHash === "string" ? expansion.childPlanHash : undefined;
266
+ const viaText = via === "task"
267
+ ? `via ${String(expansion.taskRef ?? "")}`
268
+ : via === "child"
269
+ ? `-> ${String(expansion.childRef ?? "")}${childPlanHash ? ` (plan ${childPlanHash})` : ""}`
270
+ : "direct";
271
+ lines.push(`${prefix}${ordinal}. ${stepId} ${targetKind} ${viaText}`.replace(/ +/g, " ").trimEnd());
272
+ const detailPrefix = `${prefix} `;
273
+ const inputBindings = Array.isArray(step.inputBindings) ? step.inputBindings : [];
274
+ if (inputBindings.length > 0) {
275
+ lines.push(`${detailPrefix}with: ${formatInputBindingsList(inputBindings)}`);
276
+ }
277
+ if (via === "child") {
278
+ const childOutputs = Array.isArray(expansion.childOutputs) ? expansion.childOutputs : [];
279
+ if (childOutputs.length > 0) {
280
+ lines.push(`${detailPrefix}exports: ${childOutputs.map(String).join(", ")}`);
281
+ }
282
+ const childSteps = Array.isArray(expansion.steps) ? expansion.steps : [];
283
+ for (const [index, childStep] of childSteps.entries()) {
284
+ renderPlanStepLines(childStep, index + 1, lines, `${prefix} ${ordinal}.`);
285
+ }
286
+ }
287
+ }
288
+ /**
289
+ * `akm workflow plan <ref>` text mode (P3b, spec §4.6): a human summary of
290
+ * the same compile+freeze data `--format json` returns — never a resolved
291
+ * env/secret value (B-52, B-53).
292
+ */
293
+ export function formatWorkflowPlanPlain(result) {
294
+ if (typeof result.ref !== "string")
295
+ return null;
296
+ const sourceFormat = typeof result.sourceFormat === "string" ? result.sourceFormat : "unknown";
297
+ const irVersion = typeof result.irVersion === "number" ? result.irVersion : "unknown";
298
+ const planHash = typeof result.planHash === "string" ? result.planHash : "unknown";
299
+ const execution = typeof result.execution === "object" && result.execution !== null
300
+ ? result.execution
301
+ : {};
302
+ const budget = typeof result.budget === "object" && result.budget !== null
303
+ ? result.budget
304
+ : undefined;
305
+ const lines = [
306
+ `workflow: ${result.ref} (${sourceFormat})`,
307
+ `source: ${typeof result.sourcePath === "string" ? result.sourcePath : "unknown"}`,
308
+ `plan: irVersion ${irVersion}, hash ${planHash} (not published)`,
309
+ ];
310
+ const limitsParts = [
311
+ `maxConcurrency ${typeof execution.maxConcurrency === "number" ? execution.maxConcurrency : "?"}`,
312
+ ];
313
+ if (budget) {
314
+ const budgetParts = [];
315
+ if (typeof budget.maxUnits === "number")
316
+ budgetParts.push(`max_units ${budget.maxUnits}`);
317
+ if (typeof budget.maxTokens === "number")
318
+ budgetParts.push(`max_tokens ${budget.maxTokens}`);
319
+ if (budgetParts.length > 0)
320
+ limitsParts.push(`budget ${budgetParts.join(", ")}`);
321
+ }
322
+ lines.push(`limits: ${limitsParts.join("; ")}`);
323
+ const params = Array.isArray(result.params) ? result.params : undefined;
324
+ if (params && params.length > 0)
325
+ lines.push(`params: ${params.join(", ")}`);
326
+ const outputs = typeof result.outputs === "object" && result.outputs !== null
327
+ ? result.outputs
328
+ : undefined;
329
+ if (outputs) {
330
+ for (const [name, declaration] of Object.entries(outputs)) {
331
+ const from = typeof declaration === "object" && declaration !== null
332
+ ? String(declaration.from ?? "")
333
+ : "";
334
+ lines.push(`outputs: ${name} <- ${from}`);
335
+ }
336
+ }
337
+ const steps = Array.isArray(result.steps) ? result.steps : [];
338
+ lines.push("steps:");
339
+ for (const [index, step] of steps.entries()) {
340
+ renderPlanStepLines(step, index + 1, lines);
341
+ }
342
+ const sourceReadSet = Array.isArray(result.sourceReadSet) ? result.sourceReadSet : [];
343
+ if (sourceReadSet.length > 0) {
344
+ lines.push("read set:");
345
+ for (const entry of sourceReadSet)
346
+ lines.push(` ${String(entry)}`);
347
+ }
348
+ const notices = Array.isArray(result.notices) ? result.notices : [];
349
+ if (notices.length > 0) {
350
+ lines.push("notices:");
351
+ for (const value of notices) {
352
+ const line = renderLoweringNoticeLine(value);
353
+ if (line)
354
+ lines.push(` ${line}`);
355
+ }
356
+ }
357
+ const warnings = Array.isArray(result.warnings) ? result.warnings : [];
358
+ if (warnings.length > 0) {
359
+ lines.push("warnings:");
360
+ for (const warning of warnings)
361
+ lines.push(` ! ${String(warning)}`);
362
+ }
363
+ return lines.join("\n");
364
+ }
@@ -2,7 +2,7 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  // Output text formatters for all `akm workflow *` commands.
5
- import { formatWorkflowCreatePlain, formatWorkflowListPlain, formatWorkflowResumePlain, formatWorkflowRunPlain, formatWorkflowStatusPlain, } from "./helpers.js";
5
+ import { formatWorkflowCreatePlain, formatWorkflowListPlain, formatWorkflowPlanPlain, formatWorkflowResumePlain, formatWorkflowRunPlain, formatWorkflowStatusPlain, } from "./helpers.js";
6
6
  export const workflowFormatters = [
7
7
  { command: "workflow-status", handler: (r) => formatWorkflowStatusPlain(r) },
8
8
  { command: "workflow-list", handler: (r) => formatWorkflowListPlain(r) },
@@ -10,4 +10,5 @@ export const workflowFormatters = [
10
10
  { command: "workflow-resume", handler: (r) => formatWorkflowResumePlain(r) },
11
11
  { command: "workflow-abandon", handler: (r) => formatWorkflowStatusPlain(r) },
12
12
  { command: "workflow-run", handler: (r) => formatWorkflowRunPlain(r) },
13
+ { command: "workflow-plan", handler: (r) => formatWorkflowPlanPlain(r) },
13
14
  ];
@@ -41,3 +41,7 @@ export function isSemverRange(input) {
41
41
  export function maxSatisfying(versions, range) {
42
42
  return semver.maxSatisfying(versions, range) ?? undefined;
43
43
  }
44
+ /** True when `version` satisfies `range` (both real semver forms). False for an invalid version or range. */
45
+ export function satisfiesRange(version, range) {
46
+ return semver.satisfies(version, range);
47
+ }
package/dist/runtime.js CHANGED
@@ -77,6 +77,7 @@ function nodeSpawnAdapter(cmd, options) {
77
77
  env: options.env ?? process.env,
78
78
  detached: options.detached,
79
79
  stdio: [stdioFor(options.stdin), stdioFor(options.stdout), stdioFor(options.stderr)],
80
+ windowsVerbatimArguments: options.windowsVerbatimArguments,
80
81
  });
81
82
  // Node's 'exit' fires (null, signal) when the child dies from a signal.
82
83
  // Resolving `code ?? 0` reported that as SUCCESS, so an OOM-killed or