@nklisch/pi-agile-workflow 0.15.3

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 (203) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/.codex-plugin/plugin.json +28 -0
  3. package/CHANGELOG.md +591 -0
  4. package/README.md +107 -0
  5. package/docs/ARCHITECTURE.md +716 -0
  6. package/docs/MIGRATION.md +419 -0
  7. package/docs/PRINCIPLES.md +464 -0
  8. package/docs/ROADMAP.md +95 -0
  9. package/docs/SPEC.md +855 -0
  10. package/docs/VISION.md +106 -0
  11. package/extensions/agile-workflow.test.ts +419 -0
  12. package/extensions/agile-workflow.ts +467 -0
  13. package/hooks/hooks.json +52 -0
  14. package/hooks/scripts/prompt-context.py +628 -0
  15. package/hooks/scripts/substrate-maintainer.py +359 -0
  16. package/hooks/scripts/test_prompt_context.py +757 -0
  17. package/package.json +30 -0
  18. package/scripts/install-work-view.sh +165 -0
  19. package/scripts/tests/agent-metadata.test.sh +75 -0
  20. package/scripts/tests/bump-version.test.sh +552 -0
  21. package/scripts/tests/channel-parity.test.sh +117 -0
  22. package/scripts/tests/convert-content-integrity.test.sh +246 -0
  23. package/scripts/tests/convert-install-routing.test.sh +159 -0
  24. package/scripts/tests/install-work-view.test.sh +411 -0
  25. package/scripts/tests/pi-package-metadata.test.sh +119 -0
  26. package/scripts/tests/work-board-shim.test.sh +239 -0
  27. package/scripts/tests/work-view-dist-version.test.sh +105 -0
  28. package/scripts/work-board.sh +64 -0
  29. package/scripts/work-view.sh +423 -0
  30. package/skills/autopilot/SKILL.md +330 -0
  31. package/skills/autopilot/agents/openai.yaml +6 -0
  32. package/skills/board/SKILL.md +62 -0
  33. package/skills/board/agents/openai.yaml +6 -0
  34. package/skills/bold-refactor/SKILL.md +244 -0
  35. package/skills/bold-refactor/agents/openai.yaml +6 -0
  36. package/skills/bug-scan/SKILL.md +378 -0
  37. package/skills/bug-scan/agents/openai.yaml +6 -0
  38. package/skills/bug-scan/references/async-promises.md +119 -0
  39. package/skills/bug-scan/references/concurrency-races.md +148 -0
  40. package/skills/bug-scan/references/data-layer.md +167 -0
  41. package/skills/bug-scan/references/error-handling.md +197 -0
  42. package/skills/bug-scan/references/gate-item-template.md +94 -0
  43. package/skills/bug-scan/references/language-footguns.md +219 -0
  44. package/skills/bug-scan/references/parked-item-template.md +103 -0
  45. package/skills/bug-scan/references/report-template.md +115 -0
  46. package/skills/bug-scan/references/resource-leaks.md +199 -0
  47. package/skills/bug-scan/references/state-closures.md +148 -0
  48. package/skills/bug-scan/references/time-numbers.md +158 -0
  49. package/skills/convert/SKILL.md +1463 -0
  50. package/skills/convert/agents/openai.yaml +6 -0
  51. package/skills/convert/references/legacy-overlap-migration.md +179 -0
  52. package/skills/deep-code-scan/SKILL.md +378 -0
  53. package/skills/deep-code-scan/agents/openai.yaml +6 -0
  54. package/skills/deep-code-scan/references/artifact-ledger.md +148 -0
  55. package/skills/deep-code-scan/references/consolidation.md +91 -0
  56. package/skills/deep-code-scan/references/decomposition.md +127 -0
  57. package/skills/deep-code-scan/references/item-templates.md +263 -0
  58. package/skills/deep-code-scan/references/lane-catalog.md +129 -0
  59. package/skills/deep-code-scan/references/review-gauntlet.md +108 -0
  60. package/skills/deep-code-scan/references/scanner-brief.md +152 -0
  61. package/skills/e2e-test-design/SKILL.md +517 -0
  62. package/skills/e2e-test-design/references/anti-tautology.md +261 -0
  63. package/skills/e2e-test-design/references/service-mocks.md +234 -0
  64. package/skills/e2e-test-design/references/test-taxonomy.md +211 -0
  65. package/skills/epic-design/SKILL.md +476 -0
  66. package/skills/epicize/SKILL.md +210 -0
  67. package/skills/epicize/agents/openai.yaml +6 -0
  68. package/skills/feature-design/SKILL.md +472 -0
  69. package/skills/fix/SKILL.md +173 -0
  70. package/skills/gate-cruft/SKILL.md +258 -0
  71. package/skills/gate-docs/SKILL.md +278 -0
  72. package/skills/gate-patterns/SKILL.md +389 -0
  73. package/skills/gate-refactor/SKILL.md +408 -0
  74. package/skills/gate-security/SKILL.md +275 -0
  75. package/skills/gate-tests/SKILL.md +367 -0
  76. package/skills/groom/SKILL.md +162 -0
  77. package/skills/groom/agents/openai.yaml +6 -0
  78. package/skills/ideate/SKILL.md +185 -0
  79. package/skills/ideate/agents/openai.yaml +6 -0
  80. package/skills/implement/SKILL.md +288 -0
  81. package/skills/implement-orchestrator/SKILL.md +604 -0
  82. package/skills/park/SKILL.md +111 -0
  83. package/skills/perf-design/SKILL.md +427 -0
  84. package/skills/perf-scout/SKILL.md +404 -0
  85. package/skills/perf-scout/agents/openai.yaml +6 -0
  86. package/skills/perf-scout/references/algorithmic-and-data-structures.md +126 -0
  87. package/skills/perf-scout/references/approximation-and-precomputation.md +136 -0
  88. package/skills/perf-scout/references/caching-and-memoization.md +123 -0
  89. package/skills/perf-scout/references/compiler-runtime-and-language.md +136 -0
  90. package/skills/perf-scout/references/database-and-storage-internals.md +144 -0
  91. package/skills/perf-scout/references/distributed-systems.md +150 -0
  92. package/skills/perf-scout/references/game-engine-and-realtime.md +136 -0
  93. package/skills/perf-scout/references/gpu-and-accelerators.md +130 -0
  94. package/skills/perf-scout/references/idea-ranking.md +75 -0
  95. package/skills/perf-scout/references/io-and-batching.md +135 -0
  96. package/skills/perf-scout/references/memory-and-data-locality.md +137 -0
  97. package/skills/perf-scout/references/parallelism-and-vectorization.md +126 -0
  98. package/skills/perf-scout/references/parked-item-template.md +117 -0
  99. package/skills/perf-scout/references/peer-review-pass.md +150 -0
  100. package/skills/perf-scout/references/report-template.md +148 -0
  101. package/skills/principles/SKILL.md +678 -0
  102. package/skills/principles/references/models.md +191 -0
  103. package/skills/principles/references/subagents.md +158 -0
  104. package/skills/prose-author/SKILL.md +185 -0
  105. package/skills/refactor-conventions-creator/SKILL.md +359 -0
  106. package/skills/refactor-conventions-creator/agents/openai.yaml +6 -0
  107. package/skills/refactor-conventions-creator/references/common-structures.md +91 -0
  108. package/skills/refactor-conventions-creator/references/common-styles.md +90 -0
  109. package/skills/refactor-design/SKILL.md +384 -0
  110. package/skills/release-deploy/SKILL.md +667 -0
  111. package/skills/release-deploy/agents/openai.yaml +6 -0
  112. package/skills/research/SKILL.md +166 -0
  113. package/skills/research/agents/openai.yaml +6 -0
  114. package/skills/review/SKILL.md +267 -0
  115. package/skills/review/references/deep-review.md +98 -0
  116. package/skills/review/references/review-lenses.md +68 -0
  117. package/skills/review/references/substrate-side-effects.md +161 -0
  118. package/skills/review/references/target-resolution.md +52 -0
  119. package/skills/scope/SKILL.md +486 -0
  120. package/work-view/Cargo.lock +486 -0
  121. package/work-view/Cargo.toml +9 -0
  122. package/work-view/crates/cli/.work-view-version +1 -0
  123. package/work-view/crates/cli/Cargo.toml +18 -0
  124. package/work-view/crates/cli/src/actionable.rs +500 -0
  125. package/work-view/crates/cli/src/args.rs +792 -0
  126. package/work-view/crates/cli/src/board/assets/board.css +855 -0
  127. package/work-view/crates/cli/src/board/assets/board.js +259 -0
  128. package/work-view/crates/cli/src/board/assets/card.js +139 -0
  129. package/work-view/crates/cli/src/board/assets/components.css +290 -0
  130. package/work-view/crates/cli/src/board/assets/dependency.js +1193 -0
  131. package/work-view/crates/cli/src/board/assets/detail.js +256 -0
  132. package/work-view/crates/cli/src/board/assets/filters.js +389 -0
  133. package/work-view/crates/cli/src/board/assets/index.html +77 -0
  134. package/work-view/crates/cli/src/board/assets/kanban.js +231 -0
  135. package/work-view/crates/cli/src/board/assets/markdown.js +260 -0
  136. package/work-view/crates/cli/src/board/assets/motion.css +131 -0
  137. package/work-view/crates/cli/src/board/assets/state.js +245 -0
  138. package/work-view/crates/cli/src/board/assets/table.js +304 -0
  139. package/work-view/crates/cli/src/board/assets/tokens.css +602 -0
  140. package/work-view/crates/cli/src/board/assets/views.js +41 -0
  141. package/work-view/crates/cli/src/board/assets.rs +162 -0
  142. package/work-view/crates/cli/src/board/feed.rs +204 -0
  143. package/work-view/crates/cli/src/board/mod.rs +246 -0
  144. package/work-view/crates/cli/src/board/open.rs +144 -0
  145. package/work-view/crates/cli/src/board/server.rs +407 -0
  146. package/work-view/crates/cli/src/main.rs +195 -0
  147. package/work-view/crates/cli/src/render.rs +354 -0
  148. package/work-view/crates/cli/src/scope.rs +157 -0
  149. package/work-view/crates/cli/src/stale.rs +581 -0
  150. package/work-view/crates/cli/tests/board-js/dependency-table.test.mjs +64 -0
  151. package/work-view/crates/cli/tests/board-js/expanded-browsing.test.mjs +79 -0
  152. package/work-view/crates/cli/tests/board-js/harness-smoke.test.mjs +11 -0
  153. package/work-view/crates/cli/tests/board-js/harness.mjs +437 -0
  154. package/work-view/crates/cli/tests/board-js/kanban-detail.test.mjs +75 -0
  155. package/work-view/crates/cli/tests/board-js/markdown-filter.test.mjs +60 -0
  156. package/work-view/crates/cli/tests/fixtures/golden/.work/CONVENTIONS.md +3 -0
  157. package/work-view/crates/cli/tests/fixtures/golden/.work/active/epics/epic-alpha.md +16 -0
  158. package/work-view/crates/cli/tests/fixtures/golden/.work/active/features/feat-a.md +16 -0
  159. package/work-view/crates/cli/tests/fixtures/golden/.work/active/features/feat-b.md +16 -0
  160. package/work-view/crates/cli/tests/fixtures/golden/.work/active/stories/story-alpha-1.md +16 -0
  161. package/work-view/crates/cli/tests/fixtures/golden/.work/active/stories/story-research-1.md +19 -0
  162. package/work-view/crates/cli/tests/fixtures/golden/.work/archive/feat-done.md +16 -0
  163. package/work-view/crates/cli/tests/fixtures/golden/.work/archive/feat-shipped.md +18 -0
  164. package/work-view/crates/cli/tests/fixtures/golden/.work/backlog/idea-backlog.md +7 -0
  165. package/work-view/crates/cli/tests/fixtures/golden/.work/releases/v1.0/v1.0.md +16 -0
  166. package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/CONVENTIONS.md +6 -0
  167. package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-dep-done.md +17 -0
  168. package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-design-ready.md +17 -0
  169. package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-impl-ready.md +17 -0
  170. package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/stories/story-review-ready.md +17 -0
  171. package/work-view/crates/cli/tests/fixtures/with-malformed/.work/CONVENTIONS.md +4 -0
  172. package/work-view/crates/cli/tests/fixtures/with-malformed/.work/active/epics/malformed-no-closing-fence.md +8 -0
  173. package/work-view/crates/cli/tests/fixtures/with-malformed/.work/active/features/good-item.md +16 -0
  174. package/work-view/crates/cli/tests/integration.rs +2833 -0
  175. package/work-view/crates/core/Cargo.toml +13 -0
  176. package/work-view/crates/core/src/error.rs +61 -0
  177. package/work-view/crates/core/src/filter.rs +804 -0
  178. package/work-view/crates/core/src/graph.rs +324 -0
  179. package/work-view/crates/core/src/index.rs +590 -0
  180. package/work-view/crates/core/src/lib.rs +54 -0
  181. package/work-view/crates/core/src/model.rs +202 -0
  182. package/work-view/crates/core/src/parse.rs +494 -0
  183. package/work-view/crates/core/tests/fixtures/golden/.work/CONVENTIONS.md +15 -0
  184. package/work-view/crates/core/tests/fixtures/golden/.work/active/epics/epic-core.md +16 -0
  185. package/work-view/crates/core/tests/fixtures/golden/.work/active/features/feat-parser.md +16 -0
  186. package/work-view/crates/core/tests/fixtures/golden/.work/active/features/feat-query.md +16 -0
  187. package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/malformed-no-id.md +15 -0
  188. package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/story-parse-unit.md +16 -0
  189. package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/story-security-gate.md +16 -0
  190. package/work-view/crates/core/tests/fixtures/golden/.work/archive/feat-old.md +16 -0
  191. package/work-view/crates/core/tests/fixtures/golden/.work/archive/feat-parser.md +17 -0
  192. package/work-view/crates/core/tests/fixtures/golden/.work/backlog/idea-future-board.md +7 -0
  193. package/work-view/crates/core/tests/fixtures/golden/.work/releases/v1.0/v1.0.md +16 -0
  194. package/work-view/crates/core/tests/fixtures/precedence/.work/CONVENTIONS.md +10 -0
  195. package/work-view/crates/core/tests/fixtures/precedence/.work/archive/dup-item.md +18 -0
  196. package/work-view/crates/core/tests/fixtures/precedence/.work/releases/v2.0/dup-item.md +18 -0
  197. package/work-view/crates/core/tests/integration.rs +541 -0
  198. package/work-view/dist/.gitattributes +1 -0
  199. package/work-view/dist/README.md +42 -0
  200. package/work-view/dist/aarch64-apple-darwin/work-view +0 -0
  201. package/work-view/dist/aarch64-unknown-linux-musl/work-view +0 -0
  202. package/work-view/dist/x86_64-apple-darwin/work-view +0 -0
  203. package/work-view/dist/x86_64-unknown-linux-musl/work-view +0 -0
package/docs/VISION.md ADDED
@@ -0,0 +1,106 @@
1
+ # Vision: agile-workflow
2
+
3
+ **agile-workflow** is a portable agent workflow plugin/package that ships a
4
+ markdown-based work-tracking substrate for AI-driven software projects. Work
5
+ lives as plain markdown items in a `.work/` directory inside the repo. Each item
6
+ is a single file with structured frontmatter — its body accumulates the brief,
7
+ the design, the implementation notes, and the review findings as work
8
+ progresses. Skills operate as verbs over those files, while each harness can add
9
+ native ergonomics around the same substrate. There are no parallel design docs,
10
+ no separate progress trackers, no tracking-board dependencies. The repo is the
11
+ system of record.
12
+
13
+ ## Why this exists
14
+
15
+ AI-driven software projects accumulate document sprawl: design docs that
16
+ disagree with the code, progress files that go stale, roadmaps that bind
17
+ features to releases the project never ships. The agent re-explains itself
18
+ every session because it can't trust what it reads. The user re-feeds context
19
+ because the artifacts have drifted from reality.
20
+
21
+ The substrate pattern collapses this. The item file *is* the work. Stage
22
+ advances as work happens. Foundation docs roll forward — they describe the
23
+ system as it is now or as it is intended to become, never as it was. Git
24
+ carries history; the active truth is what's in the file. An agent picking up a
25
+ fresh session reads `.work/active/`, runs the prescribed grep primitives, and
26
+ knows what's in flight without being re-fed.
27
+
28
+ ## Substrate philosophy
29
+
30
+ The plugin enforces three execution principles:
31
+
32
+ - **The item file is the work.** No parallel design docs. No separate
33
+ progress trackers. The brief, the design, the implementation notes, the
34
+ review findings — all accumulate in the item's body as stages advance.
35
+ Reading the file IS reading the state of work.
36
+ - **Foundation docs roll forward.** `docs/VISION.md`, `docs/SPEC.md`,
37
+ `docs/ARCHITECTURE.md` describe current truth or intended future state.
38
+ When implementation changes what those docs assert, the docs update to
39
+ match the new truth. No "previously this worked differently." No legacy
40
+ comments. Git is the audit trail.
41
+ - **Late-bind everything.** No upfront roadmap. No pre-populated stages.
42
+ No pre-tagged release bindings. Items advance stages when work actually
43
+ completes. Releases bind items only when the user cuts a version.
44
+
45
+ The plugin's `principles` skill loads these alongside the code-design
46
+ principles (Ports & Adapters, Single Source of Truth, Generated Contracts,
47
+ Fail Fast) carried over from `workflow`. The two paradigms — substrate
48
+ execution and code design — operate together during agile-workflow work.
49
+
50
+ ## Who this is for
51
+
52
+ Project owners running AI-driven development who want:
53
+
54
+ - A work-tracking substrate that lives with the repo, not behind a service
55
+ - Cross-session continuity without manually re-feeding context to the agent
56
+ - A pattern that scales from solo single-project to long-running
57
+ maintenance loops
58
+ - Tooling-portable substrate (git, plain markdown, a small compiled binary
59
+ with a bash fallback — no MCP, no auth, no daemon)
60
+
61
+ This is the supported workflow pattern for new projects in this repo. The older
62
+ `workflow` plugin still ships only so existing installs do not break; migrate old
63
+ projects with `/agile-workflow:convert`.
64
+
65
+ ## What success looks like now
66
+
67
+ agile-workflow is complete enough to operate as the supported workflow engine:
68
+
69
+ - 28 skills covering ideation, conversion, scoping, epic/feature design,
70
+ refactor/perf/e2e design, implementation orchestration, review, bug fixing,
71
+ board launch, bug scanning, release gates, release-deploy, and goal-backed
72
+ autopilot.
73
+ - A compiled `work-view` CLI (with a pure-bash fallback on unsupported
74
+ platforms) for fast queries by stage, tag, kind, parent, release binding,
75
+ gate origin, and dependency state.
76
+ - An interactive `work-view board` surface over the same substrate feed, with
77
+ kanban, dependency, table, detail, filter, and browsing views for humans.
78
+ - Shared Claude/Codex/Pi substrate context injection that force-loads rules at
79
+ session/compaction boundaries, surfaces prompt-time principles capsules only
80
+ for actionable workflow prompts, auto-bumps `updated:` where the harness
81
+ supports it, and reports cheap substrate validation issues.
82
+ - Three-channel plugin distribution metadata: Claude Code marketplace, OpenAI
83
+ Codex marketplace, and Pi packages share the same `skills/` source while
84
+ each channel carries native metadata and ergonomics.
85
+ - Real releases move through scope → design → implement → review →
86
+ release-deploy on the substrate; gates produce items, and the release ships
87
+ only after every bound item is terminal.
88
+
89
+ The pattern proves itself when a fresh session in a substrate-bootstrapped repo
90
+ picks up active work without re-feed, when an autopilot run drains an epic to
91
+ done autonomously across compaction events while respecting the declared
92
+ dependency graph, and when foundation docs five features later still describe
93
+ the present without legacy comments.
94
+
95
+ ## Relationship to workflow
96
+
97
+ `workflow` is deprecated and retained only for compatibility with existing
98
+ installs. New work should use `agile-workflow`; existing `workflow` projects
99
+ migrate via `/agile-workflow:convert`. Supported agile-workflow channels are
100
+ Claude Code, OpenAI Codex, and Pi; each channel adds native ergonomics around
101
+ shared skill content rather than forking the workflow model.
102
+
103
+ For depth on the principles, see [PRINCIPLES.md](./PRINCIPLES.md).
104
+ For the technical contracts, see [SPEC.md](./SPEC.md).
105
+ For the substrate's internal design, see [ARCHITECTURE.md](./ARCHITECTURE.md).
106
+ For the on-ramp from existing project shapes, see [MIGRATION.md](./MIGRATION.md).
@@ -0,0 +1,419 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import {
3
+ chmodSync,
4
+ mkdtempSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ rmSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { tmpdir } from "node:os";
12
+
13
+ import agileWorkflowExtension from "./agile-workflow";
14
+
15
+ type ExecCall = {
16
+ command: string;
17
+ args: string[];
18
+ options?: { signal?: AbortSignal; timeout?: number };
19
+ };
20
+
21
+ type RegisteredHandler = (args: string | undefined, ctx: TestContext) => Promise<string>;
22
+ type EventHandler = (event: unknown, ctx: TestContext) => unknown | Promise<unknown>;
23
+
24
+ type TestContext = {
25
+ cwd?: string;
26
+ signal?: AbortSignal;
27
+ ui?: {
28
+ notifications: Array<{ message: string; level?: string }>;
29
+ statuses: Array<{ key: string; message: string }>;
30
+ widgets: Array<{ key: string; lines: string[] }>;
31
+ notify: (message: string, level?: "info" | "warning" | "error" | "success") => void;
32
+ setStatus: (key: string, message: string) => void;
33
+ setWidget: (key: string, lines: string[]) => void;
34
+ };
35
+ };
36
+
37
+ const roots: string[] = [];
38
+
39
+ afterEach(() => {
40
+ for (const root of roots.splice(0)) {
41
+ rmSync(root, { recursive: true, force: true });
42
+ }
43
+ });
44
+
45
+ function tempRoot(): string {
46
+ const root = mkdtempSync(join(tmpdir(), "aw-extension-test-"));
47
+ roots.push(root);
48
+ return root;
49
+ }
50
+
51
+ function makeSubstrate(): { root: string; nested: string; workView: string } {
52
+ const root = tempRoot();
53
+ const workDir = join(root, ".work");
54
+ const binDir = join(workDir, "bin");
55
+ const nested = join(root, "one", "two");
56
+ mkdirSync(binDir, { recursive: true });
57
+ mkdirSync(nested, { recursive: true });
58
+ writeFileSync(join(workDir, "CONVENTIONS.md"), "# Project Conventions\n", "utf8");
59
+ const workView = join(binDir, "work-view");
60
+ writeFileSync(workView, "#!/usr/bin/env sh\nexit 0\n", "utf8");
61
+ chmodSync(workView, 0o755);
62
+ return { root, nested, workView };
63
+ }
64
+
65
+ function makeUi(): NonNullable<TestContext["ui"]> {
66
+ return {
67
+ notifications: [],
68
+ statuses: [],
69
+ widgets: [],
70
+ notify(message, level) {
71
+ this.notifications.push({ message, level });
72
+ },
73
+ setStatus(key, message) {
74
+ this.statuses.push({ key, message });
75
+ },
76
+ setWidget(key, lines) {
77
+ this.widgets.push({ key, lines });
78
+ },
79
+ };
80
+ }
81
+
82
+ function makePi(options: {
83
+ exec?: (command: string, args: string[], options?: ExecCall["options"]) => Promise<{
84
+ stdout?: string;
85
+ stderr?: string;
86
+ code?: number | null;
87
+ killed?: boolean;
88
+ }>;
89
+ sendUserMessage?: (content: string, options?: { deliverAs?: "followUp" | "steer" | "immediate" }) => void | Promise<void>;
90
+ } = {}) {
91
+ const execCalls: ExecCall[] = [];
92
+ const sentMessages: Array<{ content: string; options?: { deliverAs?: "followUp" | "steer" | "immediate" } }> = [];
93
+ const eventHandlers: Record<string, EventHandler[]> = {};
94
+ let handler: RegisteredHandler | null = null;
95
+ const pi: {
96
+ exec: (command: string, args: string[], execOptions?: ExecCall["options"]) => Promise<{
97
+ stdout?: string;
98
+ stderr?: string;
99
+ code?: number | null;
100
+ killed?: boolean;
101
+ }>;
102
+ sendUserMessage?: (content: string, options?: { deliverAs?: "followUp" | "steer" | "immediate" }) => void | Promise<void>;
103
+ registerCommand: (name: string, commandOptions: { handler: RegisteredHandler }) => void;
104
+ on: (event: string, eventHandler: EventHandler) => void;
105
+ } = {
106
+ exec: async (command: string, args: string[], execOptions?: ExecCall["options"]) => {
107
+ execCalls.push({ command, args, options: execOptions });
108
+ if (options.exec) {
109
+ return options.exec(command, args, execOptions);
110
+ }
111
+ return { stdout: "story-a\n", code: 0 };
112
+ },
113
+ registerCommand: (name: string, commandOptions: { handler: RegisteredHandler }) => {
114
+ expect(name).toBe("aw");
115
+ handler = commandOptions.handler;
116
+ },
117
+ on: (event: string, eventHandler: EventHandler) => {
118
+ (eventHandlers[event] ??= []).push(eventHandler);
119
+ },
120
+ };
121
+ if ("sendUserMessage" in options) {
122
+ if (options.sendUserMessage) {
123
+ pi.sendUserMessage = options.sendUserMessage;
124
+ }
125
+ } else {
126
+ pi.sendUserMessage = async (content, messageOptions) => {
127
+ sentMessages.push({ content, options: messageOptions });
128
+ };
129
+ }
130
+ agileWorkflowExtension(pi);
131
+ if (!handler) {
132
+ throw new Error("extension did not register /aw");
133
+ }
134
+ return {
135
+ handler,
136
+ execCalls,
137
+ sentMessages,
138
+ eventHandlers,
139
+ };
140
+ }
141
+
142
+ async function fireEvent(
143
+ handlers: Record<string, EventHandler[]>,
144
+ eventName: string,
145
+ event: unknown,
146
+ ctx: TestContext,
147
+ ): Promise<unknown> {
148
+ let result: unknown;
149
+ for (const eventHandler of handlers[eventName] ?? []) {
150
+ const next = await eventHandler(event, ctx);
151
+ if (next !== undefined) result = next;
152
+ }
153
+ return result;
154
+ }
155
+
156
+
157
+ describe("/aw command shell", () => {
158
+ test("help does not require a substrate", async () => {
159
+ const { handler, execCalls } = makePi();
160
+ const result = await handler("help", { cwd: tempRoot() });
161
+ expect(result).toContain("/aw status");
162
+ expect(result).toContain("/aw autopilot [scope]");
163
+ expect(execCalls).toHaveLength(0);
164
+ });
165
+
166
+ test("missing substrate and missing work-view return actionable warnings", async () => {
167
+ const { handler } = makePi();
168
+ const ui = makeUi();
169
+ const noSubstrate = await handler("status", { cwd: tempRoot(), ui });
170
+ expect(noSubstrate).toContain("No agile-workflow substrate found");
171
+ expect(ui.notifications.at(-1)).toMatchObject({ level: "warning" });
172
+
173
+ const root = tempRoot();
174
+ mkdirSync(join(root, ".work"), { recursive: true });
175
+ writeFileSync(join(root, ".work", "CONVENTIONS.md"), "# Project Conventions\n", "utf8");
176
+ const missingTool = await handler("status", { cwd: root, ui });
177
+ expect(missingTool).toContain("no .work/bin/work-view");
178
+ expect(ui.notifications.at(-1)).toMatchObject({ level: "warning" });
179
+ });
180
+
181
+ test("walks upward from ctx.cwd and invokes work-view with an argument array", async () => {
182
+ const substrate = makeSubstrate();
183
+ const { handler, execCalls } = makePi({
184
+ exec: async () => ({ stdout: "story-ready\n", code: 0 }),
185
+ });
186
+ const signal = new AbortController().signal;
187
+
188
+ const result = await handler("ready", { cwd: substrate.nested, signal });
189
+
190
+ expect(result).toBe("story-ready");
191
+ expect(execCalls).toEqual([
192
+ {
193
+ command: substrate.workView,
194
+ args: ["--ready"],
195
+ options: { signal, timeout: 10_000 },
196
+ },
197
+ ]);
198
+ });
199
+ });
200
+
201
+ describe("/aw queue commands", () => {
202
+ test("status composes ready, review, and blocked calls and updates Pi UI", async () => {
203
+ const substrate = makeSubstrate();
204
+ const ui = makeUi();
205
+ const { handler, execCalls } = makePi({
206
+ exec: async (_command, args) => {
207
+ const key = args.join(" ");
208
+ if (key === "--ready") {
209
+ return { stdout: "ID KIND STAGE TAGS PARENT\n-- ---- ----- ---- ------\nready-a story implementing [] -\nready-b story review [] -\n", code: 0 };
210
+ }
211
+ if (key === "--stage review") {
212
+ return { stdout: "review-a\n", code: 0 };
213
+ }
214
+ if (key === "--blocked") {
215
+ return { stdout: "", code: 0 };
216
+ }
217
+ throw new Error(`unexpected args ${key}`);
218
+ },
219
+ });
220
+
221
+ const result = await handler("status", { cwd: substrate.root, ui });
222
+
223
+ expect(execCalls.map((call) => call.args)).toEqual([
224
+ ["--ready"],
225
+ ["--stage", "review"],
226
+ ["--blocked"],
227
+ ]);
228
+ expect(result).toContain("agile-workflow: 2 ready | 1 review | 0 blocked");
229
+ expect(ui.statuses).toEqual([{ key: "agile-workflow", message: "2 ready | 1 review | 0 blocked" }]);
230
+ expect(ui.widgets.at(-1)).toEqual({
231
+ key: "agile-workflow",
232
+ lines: ["agile-workflow", "2 ready | 1 review | 0 blocked", `root: ${substrate.root}`],
233
+ });
234
+ });
235
+
236
+ test("id filters reject invalid ids before exec and pass valid ids as arguments", async () => {
237
+ const substrate = makeSubstrate();
238
+ const { handler, execCalls } = makePi();
239
+
240
+ const invalid = await handler("parent ../../oops", { cwd: substrate.root });
241
+ expect(invalid).toContain("Expected an item id for --parent");
242
+ expect(execCalls).toHaveLength(0);
243
+
244
+ await handler("blocking story-alpha", { cwd: substrate.root });
245
+ expect(execCalls).toHaveLength(1);
246
+ expect(execCalls[0].args).toEqual(["--blocking", "story-alpha"]);
247
+ });
248
+
249
+ test("large output is truncated with an explicit marker", async () => {
250
+ const substrate = makeSubstrate();
251
+ const { handler } = makePi({
252
+ exec: async () => ({ stdout: "x".repeat(6_010), code: 0 }),
253
+ });
254
+
255
+ const result = await handler("ready", { cwd: substrate.root });
256
+
257
+ expect(result.length).toBeLessThan(6_100);
258
+ expect(result).toContain("[truncated 10 chars]");
259
+ });
260
+ });
261
+
262
+ describe("Pi hook parity adapter", () => {
263
+ test("injects shared .agents/rules content and prompt-gated principles", async () => {
264
+ const substrate = makeSubstrate();
265
+ mkdirSync(join(substrate.root, ".agents", "rules"), { recursive: true });
266
+ writeFileSync(
267
+ join(substrate.root, ".agents", "rules", "agile-workflow.md"),
268
+ "## Test Rules\n- keep the shared rules source",
269
+ "utf8",
270
+ );
271
+ const { eventHandlers } = makePi();
272
+
273
+ const result = await fireEvent(
274
+ eventHandlers,
275
+ "before_agent_start",
276
+ {
277
+ prompt: "implement story-alpha",
278
+ systemPrompt: "BASE",
279
+ systemPromptOptions: { cwd: substrate.root },
280
+ },
281
+ { cwd: substrate.root },
282
+ ) as {
283
+ systemPrompt: string;
284
+ message: { customType: string; content: string; display: boolean };
285
+ };
286
+
287
+ expect(result.systemPrompt.startsWith("BASE")).toBe(true);
288
+ expect(result.systemPrompt).toContain("## Project Rules (.agents/rules/)");
289
+ expect(result.systemPrompt).toContain("keep the shared rules source");
290
+ expect(result.systemPrompt).not.toContain("## Agile Workflow Principles");
291
+ expect(result.message.customType).toBe("agile-workflow-principles");
292
+ expect(result.message.display).toBe(true);
293
+ expect(result.message.content).toContain("## Agile Workflow Principles");
294
+ expect(result.message.content).toContain("Code-design capsule");
295
+ });
296
+
297
+ test("treats bare high-intent workflow verbs as actionable", async () => {
298
+ const substrate = makeSubstrate();
299
+ const { eventHandlers } = makePi();
300
+
301
+ const result = await fireEvent(
302
+ eventHandlers,
303
+ "before_agent_start",
304
+ { prompt: "implement", systemPrompt: "BASE", systemPromptOptions: { cwd: substrate.root } },
305
+ { cwd: substrate.root },
306
+ ) as { message: { content: string } };
307
+
308
+ expect(result.message.content).toContain("## Agile Workflow Principles");
309
+ expect(result.message.content).toContain("Code-design capsule");
310
+ });
311
+
312
+ test("forces rules context every Pi turn while honoring CONVENTIONS opt-out", async () => {
313
+ const substrate = makeSubstrate();
314
+ mkdirSync(join(substrate.root, ".agents", "rules"), { recursive: true });
315
+ writeFileSync(join(substrate.root, ".agents", "rules", "project.md"), "## Rules\n- loaded each turn", "utf8");
316
+ const { eventHandlers } = makePi();
317
+
318
+ const first = await fireEvent(
319
+ eventHandlers,
320
+ "before_agent_start",
321
+ { prompt: "explain this repo", systemPrompt: "BASE", systemPromptOptions: { cwd: substrate.root } },
322
+ { cwd: substrate.root },
323
+ ) as { systemPrompt: string };
324
+ const second = await fireEvent(
325
+ eventHandlers,
326
+ "before_agent_start",
327
+ { prompt: "explain this repo", systemPrompt: "BASE", systemPromptOptions: { cwd: substrate.root } },
328
+ { cwd: substrate.root },
329
+ ) as { systemPrompt: string };
330
+
331
+ expect(first.systemPrompt).toContain("loaded each turn");
332
+ expect(second.systemPrompt).toContain("loaded each turn");
333
+
334
+ writeFileSync(join(substrate.root, ".work", "CONVENTIONS.md"), "rules_context: off\n", "utf8");
335
+ const disabled = await fireEvent(
336
+ eventHandlers,
337
+ "before_agent_start",
338
+ { prompt: "explain this repo", systemPrompt: "BASE", systemPromptOptions: { cwd: substrate.root } },
339
+ { cwd: substrate.root },
340
+ );
341
+ expect(disabled).toBeUndefined();
342
+ });
343
+
344
+ test("runs substrate maintenance after mutating tools", async () => {
345
+ const substrate = makeSubstrate();
346
+ const storyDir = join(substrate.root, ".work", "active", "stories");
347
+ mkdirSync(storyDir, { recursive: true });
348
+ const storyPath = join(storyDir, "story-alpha.md");
349
+ writeFileSync(
350
+ storyPath,
351
+ [
352
+ "---",
353
+ "id: story-alpha",
354
+ "stage: implementing",
355
+ "tags: []",
356
+ "parent: null",
357
+ "depends_on: []",
358
+ "release_binding: null",
359
+ "gate_origin: null",
360
+ "created: 2000-01-01",
361
+ "updated: 2000-01-01",
362
+ "---",
363
+ "# Story Alpha",
364
+ ].join("\n"),
365
+ "utf8",
366
+ );
367
+ const { eventHandlers } = makePi();
368
+
369
+ const result = await fireEvent(
370
+ eventHandlers,
371
+ "tool_result",
372
+ {
373
+ toolName: "edit",
374
+ input: { path: storyPath },
375
+ content: [{ type: "text", text: "edited" }],
376
+ },
377
+ { cwd: substrate.root },
378
+ ) as { content: Array<{ type: string; text: string }> };
379
+
380
+ const updatedStory = readFileSync(storyPath, "utf8");
381
+ expect(updatedStory).toMatch(/updated: \d{4}-\d{2}-\d{2}/);
382
+ expect(updatedStory).not.toContain("updated: 2000-01-01");
383
+ expect(result.content.at(-1)?.text).toContain("Agile Workflow substrate validation found issues");
384
+ expect(result.content.at(-1)?.text).toContain("missing required frontmatter fields: kind");
385
+ });
386
+ });
387
+
388
+ describe("/aw workflow handoffs", () => {
389
+ test("queues board, autopilot, and scope through Pi follow-up messages", async () => {
390
+ const substrate = makeSubstrate();
391
+ const { handler, sentMessages } = makePi();
392
+
393
+ await handler("board", { cwd: substrate.root });
394
+ await handler("autopilot", { cwd: substrate.root });
395
+ await handler("autopilot feature-alpha", { cwd: substrate.root });
396
+ await handler("scope capture this idea", { cwd: substrate.root });
397
+
398
+ expect(sentMessages).toEqual([
399
+ { content: "$agile-workflow:board", options: { deliverAs: "followUp" } },
400
+ { content: "$agile-workflow:autopilot --all", options: { deliverAs: "followUp" } },
401
+ { content: "$agile-workflow:autopilot feature-alpha", options: { deliverAs: "followUp" } },
402
+ { content: "$agile-workflow:scope capture this idea", options: { deliverAs: "followUp" } },
403
+ ]);
404
+ });
405
+
406
+ test("scope requires text and handoffs fall back when follow-ups are unavailable", async () => {
407
+ const substrate = makeSubstrate();
408
+ const ui = makeUi();
409
+ const { handler, sentMessages } = makePi({ sendUserMessage: undefined });
410
+
411
+ const missingScope = await handler("scope", { cwd: substrate.root, ui });
412
+ expect(missingScope).toContain("Expected an idea or item id for /aw scope");
413
+
414
+ const fallback = await handler("board", { cwd: substrate.root, ui });
415
+ expect(fallback).toContain("Pi follow-up messages are unavailable");
416
+ expect(fallback).toContain("$agile-workflow:board");
417
+ expect(sentMessages).toHaveLength(0);
418
+ });
419
+ });