@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.
- package/.claude-plugin/plugin.json +10 -0
- package/.codex-plugin/plugin.json +28 -0
- package/CHANGELOG.md +591 -0
- package/README.md +107 -0
- package/docs/ARCHITECTURE.md +716 -0
- package/docs/MIGRATION.md +419 -0
- package/docs/PRINCIPLES.md +464 -0
- package/docs/ROADMAP.md +95 -0
- package/docs/SPEC.md +855 -0
- package/docs/VISION.md +106 -0
- package/extensions/agile-workflow.test.ts +419 -0
- package/extensions/agile-workflow.ts +467 -0
- package/hooks/hooks.json +52 -0
- package/hooks/scripts/prompt-context.py +628 -0
- package/hooks/scripts/substrate-maintainer.py +359 -0
- package/hooks/scripts/test_prompt_context.py +757 -0
- package/package.json +30 -0
- package/scripts/install-work-view.sh +165 -0
- package/scripts/tests/agent-metadata.test.sh +75 -0
- package/scripts/tests/bump-version.test.sh +552 -0
- package/scripts/tests/channel-parity.test.sh +117 -0
- package/scripts/tests/convert-content-integrity.test.sh +246 -0
- package/scripts/tests/convert-install-routing.test.sh +159 -0
- package/scripts/tests/install-work-view.test.sh +411 -0
- package/scripts/tests/pi-package-metadata.test.sh +119 -0
- package/scripts/tests/work-board-shim.test.sh +239 -0
- package/scripts/tests/work-view-dist-version.test.sh +105 -0
- package/scripts/work-board.sh +64 -0
- package/scripts/work-view.sh +423 -0
- package/skills/autopilot/SKILL.md +330 -0
- package/skills/autopilot/agents/openai.yaml +6 -0
- package/skills/board/SKILL.md +62 -0
- package/skills/board/agents/openai.yaml +6 -0
- package/skills/bold-refactor/SKILL.md +244 -0
- package/skills/bold-refactor/agents/openai.yaml +6 -0
- package/skills/bug-scan/SKILL.md +378 -0
- package/skills/bug-scan/agents/openai.yaml +6 -0
- package/skills/bug-scan/references/async-promises.md +119 -0
- package/skills/bug-scan/references/concurrency-races.md +148 -0
- package/skills/bug-scan/references/data-layer.md +167 -0
- package/skills/bug-scan/references/error-handling.md +197 -0
- package/skills/bug-scan/references/gate-item-template.md +94 -0
- package/skills/bug-scan/references/language-footguns.md +219 -0
- package/skills/bug-scan/references/parked-item-template.md +103 -0
- package/skills/bug-scan/references/report-template.md +115 -0
- package/skills/bug-scan/references/resource-leaks.md +199 -0
- package/skills/bug-scan/references/state-closures.md +148 -0
- package/skills/bug-scan/references/time-numbers.md +158 -0
- package/skills/convert/SKILL.md +1463 -0
- package/skills/convert/agents/openai.yaml +6 -0
- package/skills/convert/references/legacy-overlap-migration.md +179 -0
- package/skills/deep-code-scan/SKILL.md +378 -0
- package/skills/deep-code-scan/agents/openai.yaml +6 -0
- package/skills/deep-code-scan/references/artifact-ledger.md +148 -0
- package/skills/deep-code-scan/references/consolidation.md +91 -0
- package/skills/deep-code-scan/references/decomposition.md +127 -0
- package/skills/deep-code-scan/references/item-templates.md +263 -0
- package/skills/deep-code-scan/references/lane-catalog.md +129 -0
- package/skills/deep-code-scan/references/review-gauntlet.md +108 -0
- package/skills/deep-code-scan/references/scanner-brief.md +152 -0
- package/skills/e2e-test-design/SKILL.md +517 -0
- package/skills/e2e-test-design/references/anti-tautology.md +261 -0
- package/skills/e2e-test-design/references/service-mocks.md +234 -0
- package/skills/e2e-test-design/references/test-taxonomy.md +211 -0
- package/skills/epic-design/SKILL.md +476 -0
- package/skills/epicize/SKILL.md +210 -0
- package/skills/epicize/agents/openai.yaml +6 -0
- package/skills/feature-design/SKILL.md +472 -0
- package/skills/fix/SKILL.md +173 -0
- package/skills/gate-cruft/SKILL.md +258 -0
- package/skills/gate-docs/SKILL.md +278 -0
- package/skills/gate-patterns/SKILL.md +389 -0
- package/skills/gate-refactor/SKILL.md +408 -0
- package/skills/gate-security/SKILL.md +275 -0
- package/skills/gate-tests/SKILL.md +367 -0
- package/skills/groom/SKILL.md +162 -0
- package/skills/groom/agents/openai.yaml +6 -0
- package/skills/ideate/SKILL.md +185 -0
- package/skills/ideate/agents/openai.yaml +6 -0
- package/skills/implement/SKILL.md +288 -0
- package/skills/implement-orchestrator/SKILL.md +604 -0
- package/skills/park/SKILL.md +111 -0
- package/skills/perf-design/SKILL.md +427 -0
- package/skills/perf-scout/SKILL.md +404 -0
- package/skills/perf-scout/agents/openai.yaml +6 -0
- package/skills/perf-scout/references/algorithmic-and-data-structures.md +126 -0
- package/skills/perf-scout/references/approximation-and-precomputation.md +136 -0
- package/skills/perf-scout/references/caching-and-memoization.md +123 -0
- package/skills/perf-scout/references/compiler-runtime-and-language.md +136 -0
- package/skills/perf-scout/references/database-and-storage-internals.md +144 -0
- package/skills/perf-scout/references/distributed-systems.md +150 -0
- package/skills/perf-scout/references/game-engine-and-realtime.md +136 -0
- package/skills/perf-scout/references/gpu-and-accelerators.md +130 -0
- package/skills/perf-scout/references/idea-ranking.md +75 -0
- package/skills/perf-scout/references/io-and-batching.md +135 -0
- package/skills/perf-scout/references/memory-and-data-locality.md +137 -0
- package/skills/perf-scout/references/parallelism-and-vectorization.md +126 -0
- package/skills/perf-scout/references/parked-item-template.md +117 -0
- package/skills/perf-scout/references/peer-review-pass.md +150 -0
- package/skills/perf-scout/references/report-template.md +148 -0
- package/skills/principles/SKILL.md +678 -0
- package/skills/principles/references/models.md +191 -0
- package/skills/principles/references/subagents.md +158 -0
- package/skills/prose-author/SKILL.md +185 -0
- package/skills/refactor-conventions-creator/SKILL.md +359 -0
- package/skills/refactor-conventions-creator/agents/openai.yaml +6 -0
- package/skills/refactor-conventions-creator/references/common-structures.md +91 -0
- package/skills/refactor-conventions-creator/references/common-styles.md +90 -0
- package/skills/refactor-design/SKILL.md +384 -0
- package/skills/release-deploy/SKILL.md +667 -0
- package/skills/release-deploy/agents/openai.yaml +6 -0
- package/skills/research/SKILL.md +166 -0
- package/skills/research/agents/openai.yaml +6 -0
- package/skills/review/SKILL.md +267 -0
- package/skills/review/references/deep-review.md +98 -0
- package/skills/review/references/review-lenses.md +68 -0
- package/skills/review/references/substrate-side-effects.md +161 -0
- package/skills/review/references/target-resolution.md +52 -0
- package/skills/scope/SKILL.md +486 -0
- package/work-view/Cargo.lock +486 -0
- package/work-view/Cargo.toml +9 -0
- package/work-view/crates/cli/.work-view-version +1 -0
- package/work-view/crates/cli/Cargo.toml +18 -0
- package/work-view/crates/cli/src/actionable.rs +500 -0
- package/work-view/crates/cli/src/args.rs +792 -0
- package/work-view/crates/cli/src/board/assets/board.css +855 -0
- package/work-view/crates/cli/src/board/assets/board.js +259 -0
- package/work-view/crates/cli/src/board/assets/card.js +139 -0
- package/work-view/crates/cli/src/board/assets/components.css +290 -0
- package/work-view/crates/cli/src/board/assets/dependency.js +1193 -0
- package/work-view/crates/cli/src/board/assets/detail.js +256 -0
- package/work-view/crates/cli/src/board/assets/filters.js +389 -0
- package/work-view/crates/cli/src/board/assets/index.html +77 -0
- package/work-view/crates/cli/src/board/assets/kanban.js +231 -0
- package/work-view/crates/cli/src/board/assets/markdown.js +260 -0
- package/work-view/crates/cli/src/board/assets/motion.css +131 -0
- package/work-view/crates/cli/src/board/assets/state.js +245 -0
- package/work-view/crates/cli/src/board/assets/table.js +304 -0
- package/work-view/crates/cli/src/board/assets/tokens.css +602 -0
- package/work-view/crates/cli/src/board/assets/views.js +41 -0
- package/work-view/crates/cli/src/board/assets.rs +162 -0
- package/work-view/crates/cli/src/board/feed.rs +204 -0
- package/work-view/crates/cli/src/board/mod.rs +246 -0
- package/work-view/crates/cli/src/board/open.rs +144 -0
- package/work-view/crates/cli/src/board/server.rs +407 -0
- package/work-view/crates/cli/src/main.rs +195 -0
- package/work-view/crates/cli/src/render.rs +354 -0
- package/work-view/crates/cli/src/scope.rs +157 -0
- package/work-view/crates/cli/src/stale.rs +581 -0
- package/work-view/crates/cli/tests/board-js/dependency-table.test.mjs +64 -0
- package/work-view/crates/cli/tests/board-js/expanded-browsing.test.mjs +79 -0
- package/work-view/crates/cli/tests/board-js/harness-smoke.test.mjs +11 -0
- package/work-view/crates/cli/tests/board-js/harness.mjs +437 -0
- package/work-view/crates/cli/tests/board-js/kanban-detail.test.mjs +75 -0
- package/work-view/crates/cli/tests/board-js/markdown-filter.test.mjs +60 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/CONVENTIONS.md +3 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/epics/epic-alpha.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/features/feat-a.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/features/feat-b.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/stories/story-alpha-1.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/stories/story-research-1.md +19 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/archive/feat-done.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/archive/feat-shipped.md +18 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/backlog/idea-backlog.md +7 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/releases/v1.0/v1.0.md +16 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/CONVENTIONS.md +6 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-dep-done.md +17 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-design-ready.md +17 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-impl-ready.md +17 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/stories/story-review-ready.md +17 -0
- package/work-view/crates/cli/tests/fixtures/with-malformed/.work/CONVENTIONS.md +4 -0
- package/work-view/crates/cli/tests/fixtures/with-malformed/.work/active/epics/malformed-no-closing-fence.md +8 -0
- package/work-view/crates/cli/tests/fixtures/with-malformed/.work/active/features/good-item.md +16 -0
- package/work-view/crates/cli/tests/integration.rs +2833 -0
- package/work-view/crates/core/Cargo.toml +13 -0
- package/work-view/crates/core/src/error.rs +61 -0
- package/work-view/crates/core/src/filter.rs +804 -0
- package/work-view/crates/core/src/graph.rs +324 -0
- package/work-view/crates/core/src/index.rs +590 -0
- package/work-view/crates/core/src/lib.rs +54 -0
- package/work-view/crates/core/src/model.rs +202 -0
- package/work-view/crates/core/src/parse.rs +494 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/CONVENTIONS.md +15 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/epics/epic-core.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/features/feat-parser.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/features/feat-query.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/malformed-no-id.md +15 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/story-parse-unit.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/story-security-gate.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/archive/feat-old.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/archive/feat-parser.md +17 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/backlog/idea-future-board.md +7 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/releases/v1.0/v1.0.md +16 -0
- package/work-view/crates/core/tests/fixtures/precedence/.work/CONVENTIONS.md +10 -0
- package/work-view/crates/core/tests/fixtures/precedence/.work/archive/dup-item.md +18 -0
- package/work-view/crates/core/tests/fixtures/precedence/.work/releases/v2.0/dup-item.md +18 -0
- package/work-view/crates/core/tests/integration.rs +541 -0
- package/work-view/dist/.gitattributes +1 -0
- package/work-view/dist/README.md +42 -0
- package/work-view/dist/aarch64-apple-darwin/work-view +0 -0
- package/work-view/dist/aarch64-unknown-linux-musl/work-view +0 -0
- package/work-view/dist/x86_64-apple-darwin/work-view +0 -0
- package/work-view/dist/x86_64-unknown-linux-musl/work-view +0 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const STATUS_KEY = "agile-workflow";
|
|
7
|
+
const MAX_OUTPUT_CHARS = 6_000;
|
|
8
|
+
const ITEM_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
9
|
+
const HOOK_TIMEOUT_MS = 5_000;
|
|
10
|
+
const RULES_CONTEXT_HEADER = "## Project Rules (.agents/rules/)";
|
|
11
|
+
const PRINCIPLES_CONTEXT_HEADER = "## Agile Workflow Principles";
|
|
12
|
+
const MUTATING_TOOL_NAMES = new Set(["write", "edit", "apply_patch"]);
|
|
13
|
+
|
|
14
|
+
const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
15
|
+
const PROMPT_CONTEXT_SCRIPT = join(PLUGIN_ROOT, "hooks", "scripts", "prompt-context.py");
|
|
16
|
+
const SUBSTRATE_MAINTAINER_SCRIPT = join(PLUGIN_ROOT, "hooks", "scripts", "substrate-maintainer.py");
|
|
17
|
+
|
|
18
|
+
type PiApi = {
|
|
19
|
+
exec: (
|
|
20
|
+
command: string,
|
|
21
|
+
args: string[],
|
|
22
|
+
options?: { signal?: AbortSignal; timeout?: number },
|
|
23
|
+
) => Promise<{ stdout?: string; stderr?: string; code?: number | null; killed?: boolean }>;
|
|
24
|
+
sendUserMessage?: (
|
|
25
|
+
content: string,
|
|
26
|
+
options?: { deliverAs?: "followUp" | "steer" | "immediate" },
|
|
27
|
+
) => void | Promise<void>;
|
|
28
|
+
registerCommand: (
|
|
29
|
+
name: string,
|
|
30
|
+
options: { description: string; handler: (args: string | undefined, ctx: PiContext) => Promise<string> },
|
|
31
|
+
) => void;
|
|
32
|
+
on?: (
|
|
33
|
+
event: string,
|
|
34
|
+
handler: (event: unknown, ctx: PiContext) => unknown | Promise<unknown>,
|
|
35
|
+
) => void;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type PiContext = {
|
|
39
|
+
cwd?: string;
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
sessionManager?: {
|
|
42
|
+
getSessionFile?: () => string | undefined;
|
|
43
|
+
};
|
|
44
|
+
ui?: {
|
|
45
|
+
notify?: (message: string, level?: "info" | "warning" | "error" | "success") => void;
|
|
46
|
+
setStatus?: (key: string, message: string) => void;
|
|
47
|
+
setWidget?: (key: string, lines: string[]) => void;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type BeforeAgentStartEvent = {
|
|
52
|
+
prompt?: string;
|
|
53
|
+
systemPrompt?: string;
|
|
54
|
+
systemPromptOptions?: { cwd?: string };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type BeforeAgentStartResult = {
|
|
58
|
+
systemPrompt?: string;
|
|
59
|
+
message?: {
|
|
60
|
+
customType: string;
|
|
61
|
+
content: string;
|
|
62
|
+
display: boolean;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type SessionEvent = {
|
|
67
|
+
reason?: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type ToolResultEvent = {
|
|
71
|
+
toolName?: string;
|
|
72
|
+
input?: unknown;
|
|
73
|
+
content?: unknown;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
type HookOutput = {
|
|
77
|
+
hookSpecificOutput?: {
|
|
78
|
+
additionalContext?: unknown;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type Substrate = {
|
|
83
|
+
root: string;
|
|
84
|
+
workView: string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function registerHookParity(pi: PiApi): void {
|
|
88
|
+
// Claude Code and Codex run hooks/hooks.json directly. Pi has no package hook
|
|
89
|
+
// manifest, so this adapter maps Pi-native events onto the SAME deterministic
|
|
90
|
+
// Python hook scripts instead of reimplementing workflow rules in TypeScript.
|
|
91
|
+
// That keeps .agents/rules loading, principles capsules, updated: bumps, and
|
|
92
|
+
// cheap substrate validation behaviorally aligned across all three channels.
|
|
93
|
+
pi.on?.("session_start", (event, ctx) => {
|
|
94
|
+
runPromptContext(
|
|
95
|
+
{
|
|
96
|
+
hook_event_name: "SessionStart",
|
|
97
|
+
source: sessionSource(event),
|
|
98
|
+
cwd: ctx.cwd,
|
|
99
|
+
session_id: sessionId(ctx),
|
|
100
|
+
},
|
|
101
|
+
ctx.cwd,
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
pi.on?.("session_compact", (event, ctx) => {
|
|
106
|
+
runPromptContext(
|
|
107
|
+
{
|
|
108
|
+
hook_event_name: "PostCompact",
|
|
109
|
+
source: "compact",
|
|
110
|
+
trigger: sessionSource(event),
|
|
111
|
+
cwd: ctx.cwd,
|
|
112
|
+
session_id: sessionId(ctx),
|
|
113
|
+
},
|
|
114
|
+
ctx.cwd,
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
pi.on?.("before_agent_start", (rawEvent, ctx): BeforeAgentStartResult | undefined => {
|
|
119
|
+
const event = (rawEvent ?? {}) as BeforeAgentStartEvent;
|
|
120
|
+
const cwd = event.systemPromptOptions?.cwd ?? ctx.cwd ?? process.cwd();
|
|
121
|
+
const session_id = sessionId(ctx);
|
|
122
|
+
const base = event.systemPrompt ?? "";
|
|
123
|
+
const result: BeforeAgentStartResult = {};
|
|
124
|
+
|
|
125
|
+
if (!base.includes(RULES_CONTEXT_HEADER)) {
|
|
126
|
+
const rules = runPromptContext(
|
|
127
|
+
{
|
|
128
|
+
hook_event_name: "PiBeforeAgentStart",
|
|
129
|
+
source: "pi-before-agent-start",
|
|
130
|
+
cwd,
|
|
131
|
+
session_id,
|
|
132
|
+
force_rules_context: true,
|
|
133
|
+
},
|
|
134
|
+
cwd,
|
|
135
|
+
);
|
|
136
|
+
if (rules) {
|
|
137
|
+
result.systemPrompt = `${base}\n\n${rules}`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const effectivePrompt = result.systemPrompt ?? base;
|
|
142
|
+
if (!effectivePrompt.includes(PRINCIPLES_CONTEXT_HEADER)) {
|
|
143
|
+
const principles = runPromptContext(
|
|
144
|
+
{
|
|
145
|
+
hook_event_name: "UserPromptSubmit",
|
|
146
|
+
source: "user",
|
|
147
|
+
cwd,
|
|
148
|
+
session_id,
|
|
149
|
+
prompt: event.prompt ?? "",
|
|
150
|
+
},
|
|
151
|
+
cwd,
|
|
152
|
+
);
|
|
153
|
+
if (principles) {
|
|
154
|
+
// Hook-specific context in Claude/Codex lands as an injected context
|
|
155
|
+
// message, not as invisible permanent instructions. Use the same shape
|
|
156
|
+
// in Pi so the model can actually see prompt-gated principles capsules
|
|
157
|
+
// in the conversation stream while keeping the bulky rules block in the
|
|
158
|
+
// rebuilt system prompt.
|
|
159
|
+
result.message = {
|
|
160
|
+
customType: "agile-workflow-principles",
|
|
161
|
+
content: principles,
|
|
162
|
+
display: true,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!result.systemPrompt && !result.message) return;
|
|
168
|
+
return result;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
pi.on?.("tool_result", (rawEvent, ctx) => {
|
|
172
|
+
const event = (rawEvent ?? {}) as ToolResultEvent;
|
|
173
|
+
const toolName = String(event.toolName ?? "").toLowerCase();
|
|
174
|
+
if (!MUTATING_TOOL_NAMES.has(toolName)) return;
|
|
175
|
+
|
|
176
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
177
|
+
const context = runHookScript(
|
|
178
|
+
SUBSTRATE_MAINTAINER_SCRIPT,
|
|
179
|
+
{
|
|
180
|
+
hook_event_name: "PostToolUse",
|
|
181
|
+
source: "pi-tool-result",
|
|
182
|
+
cwd,
|
|
183
|
+
session_id: sessionId(ctx),
|
|
184
|
+
tool_input: event.input ?? {},
|
|
185
|
+
},
|
|
186
|
+
cwd,
|
|
187
|
+
);
|
|
188
|
+
if (!context) return;
|
|
189
|
+
return appendToolContext(event.content, context);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export default function agileWorkflowExtension(pi: PiApi) {
|
|
194
|
+
registerHookParity(pi);
|
|
195
|
+
|
|
196
|
+
pi.registerCommand("aw", {
|
|
197
|
+
description: "Inspect and navigate the agile-workflow .work substrate",
|
|
198
|
+
handler: async (args, ctx) => {
|
|
199
|
+
const input = (args ?? "").trim();
|
|
200
|
+
const [command] = input.split(/\s+/, 1);
|
|
201
|
+
const rest = command ? input.slice(command.length).trim() : "";
|
|
202
|
+
|
|
203
|
+
if (!command || command === "help" || command === "-h" || command === "--help") {
|
|
204
|
+
return show(ctx, helpText());
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const substrate = findSubstrate(ctx.cwd);
|
|
208
|
+
if (!substrate) {
|
|
209
|
+
return show(
|
|
210
|
+
ctx,
|
|
211
|
+
"No agile-workflow substrate found. Run $agile-workflow:convert in this repo before using /aw.",
|
|
212
|
+
"warning",
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!existsSync(substrate.workView)) {
|
|
217
|
+
return show(
|
|
218
|
+
ctx,
|
|
219
|
+
"Found .work/ but no .work/bin/work-view. Run $agile-workflow:convert or reinstall the substrate tooling.",
|
|
220
|
+
"warning",
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
switch (command) {
|
|
226
|
+
case "status":
|
|
227
|
+
return show(ctx, await queueSnapshot(pi, ctx, substrate));
|
|
228
|
+
case "ready":
|
|
229
|
+
return show(ctx, await runWorkView(pi, ctx, substrate, ["--ready"]));
|
|
230
|
+
case "blocked":
|
|
231
|
+
return show(ctx, await runWorkView(pi, ctx, substrate, ["--blocked"]));
|
|
232
|
+
case "review":
|
|
233
|
+
return show(ctx, await runWorkView(pi, ctx, substrate, ["--stage", "review"]));
|
|
234
|
+
case "parent":
|
|
235
|
+
return show(ctx, await runIdFilter(pi, ctx, substrate, "--parent", rest));
|
|
236
|
+
case "blocking":
|
|
237
|
+
return show(ctx, await runIdFilter(pi, ctx, substrate, "--blocking", rest));
|
|
238
|
+
case "board":
|
|
239
|
+
return show(ctx, await queueSkillHandoff(pi, "$agile-workflow:board"));
|
|
240
|
+
case "autopilot":
|
|
241
|
+
return show(ctx, await queueSkillHandoff(pi, `$agile-workflow:autopilot ${rest || "--all"}`));
|
|
242
|
+
case "scope":
|
|
243
|
+
if (!rest) {
|
|
244
|
+
return show(ctx, "Expected an idea or item id for /aw scope.\n\n" + helpText(), "warning");
|
|
245
|
+
}
|
|
246
|
+
return show(ctx, await queueSkillHandoff(pi, `$agile-workflow:scope ${rest}`));
|
|
247
|
+
default:
|
|
248
|
+
return show(
|
|
249
|
+
ctx,
|
|
250
|
+
`Unknown /aw subcommand: ${command}\n\n${helpText()}`,
|
|
251
|
+
"warning",
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
} catch (error) {
|
|
255
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
256
|
+
return show(ctx, `agile-workflow command failed: ${message}`, "error");
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function sessionSource(event: unknown): string {
|
|
263
|
+
const reason = (event as SessionEvent | undefined)?.reason;
|
|
264
|
+
return typeof reason === "string" && reason ? reason : "startup";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function sessionId(ctx: PiContext): string {
|
|
268
|
+
try {
|
|
269
|
+
const file = ctx.sessionManager?.getSessionFile?.();
|
|
270
|
+
if (file) return file;
|
|
271
|
+
} catch {
|
|
272
|
+
// Fail open: hook state dedup is a convenience, never a reason to break a turn.
|
|
273
|
+
}
|
|
274
|
+
return "pi-session";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function runPromptContext(payload: Record<string, unknown>, cwd?: string): string {
|
|
278
|
+
return runHookScript(PROMPT_CONTEXT_SCRIPT, payload, cwd);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function runHookScript(script: string, payload: Record<string, unknown>, cwd?: string): string {
|
|
282
|
+
if (!existsSync(script)) return "";
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const result = spawnSync("python3", [script], {
|
|
286
|
+
cwd: cwd || process.cwd(),
|
|
287
|
+
encoding: "utf8",
|
|
288
|
+
env: {
|
|
289
|
+
...process.env,
|
|
290
|
+
PYTHONDONTWRITEBYTECODE: "1",
|
|
291
|
+
PLUGIN_ROOT,
|
|
292
|
+
CLAUDE_PLUGIN_ROOT: process.env.CLAUDE_PLUGIN_ROOT || PLUGIN_ROOT,
|
|
293
|
+
},
|
|
294
|
+
input: JSON.stringify(payload),
|
|
295
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
296
|
+
timeout: HOOK_TIMEOUT_MS,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
if (result.error || (typeof result.status === "number" && result.status !== 0)) {
|
|
300
|
+
return "";
|
|
301
|
+
}
|
|
302
|
+
return parseHookContext(result.stdout || "");
|
|
303
|
+
} catch {
|
|
304
|
+
return "";
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function parseHookContext(stdout: string): string {
|
|
309
|
+
const raw = stdout.trim();
|
|
310
|
+
if (!raw) return "";
|
|
311
|
+
const line = raw
|
|
312
|
+
.split(/\r?\n/)
|
|
313
|
+
.reverse()
|
|
314
|
+
.find((candidate) => candidate.trim().startsWith("{"));
|
|
315
|
+
if (!line) return "";
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
const parsed = JSON.parse(line) as HookOutput;
|
|
319
|
+
const context = parsed.hookSpecificOutput?.additionalContext;
|
|
320
|
+
return typeof context === "string" ? context.trim() : "";
|
|
321
|
+
} catch {
|
|
322
|
+
return "";
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function appendToolContext(content: unknown, context: string): { content: Array<unknown> } {
|
|
327
|
+
const note = { type: "text", text: `\n\n${context}` };
|
|
328
|
+
if (Array.isArray(content)) {
|
|
329
|
+
return { content: [...content, note] };
|
|
330
|
+
}
|
|
331
|
+
if (typeof content === "string" && content) {
|
|
332
|
+
return { content: [{ type: "text", text: content }, note] };
|
|
333
|
+
}
|
|
334
|
+
if (content && typeof content === "object") {
|
|
335
|
+
return { content: [content, note] };
|
|
336
|
+
}
|
|
337
|
+
return { content: [note] };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function findSubstrate(start?: string): Substrate | null {
|
|
341
|
+
let current = resolve(start || process.cwd());
|
|
342
|
+
|
|
343
|
+
while (true) {
|
|
344
|
+
const conventions = join(current, ".work", "CONVENTIONS.md");
|
|
345
|
+
if (existsSync(conventions)) {
|
|
346
|
+
return {
|
|
347
|
+
root: current,
|
|
348
|
+
workView: join(current, ".work", "bin", "work-view"),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const parent = dirname(current);
|
|
353
|
+
if (parent === current) return null;
|
|
354
|
+
current = parent;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function runWorkView(pi: PiApi, ctx: PiContext, substrate: Substrate, args: string[]): Promise<string> {
|
|
359
|
+
const result = await pi.exec(substrate.workView, args, {
|
|
360
|
+
signal: ctx.signal,
|
|
361
|
+
timeout: 10_000,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
if (result.killed) {
|
|
365
|
+
throw new Error("work-view timed out");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (result.code && result.code !== 0) {
|
|
369
|
+
const stderr = (result.stderr || "").trim();
|
|
370
|
+
throw new Error(stderr || `work-view exited with code ${result.code}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return (result.stdout || "").trim() || "(no matching items)";
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function runIdFilter(
|
|
377
|
+
pi: PiApi,
|
|
378
|
+
ctx: PiContext,
|
|
379
|
+
substrate: Substrate,
|
|
380
|
+
flag: "--parent" | "--blocking",
|
|
381
|
+
id: string,
|
|
382
|
+
): Promise<string> {
|
|
383
|
+
const trimmed = id.trim();
|
|
384
|
+
if (!ITEM_ID_RE.test(trimmed)) {
|
|
385
|
+
return `Expected an item id for ${flag}.\n\n${helpText()}`;
|
|
386
|
+
}
|
|
387
|
+
return runWorkView(pi, ctx, substrate, [flag, trimmed]);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function queueSkillHandoff(pi: PiApi, invocation: string): Promise<string> {
|
|
391
|
+
if (pi.sendUserMessage) {
|
|
392
|
+
await pi.sendUserMessage(invocation, { deliverAs: "followUp" });
|
|
393
|
+
return `Queued follow-up: ${invocation}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return `Pi follow-up messages are unavailable in this runtime. Run: ${invocation}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function queueSnapshot(pi: PiApi, ctx: PiContext, substrate: Substrate): Promise<string> {
|
|
400
|
+
const ready = await runWorkView(pi, ctx, substrate, ["--ready"]);
|
|
401
|
+
const review = await runWorkView(pi, ctx, substrate, ["--stage", "review"]);
|
|
402
|
+
const blocked = await runWorkView(pi, ctx, substrate, ["--blocked"]);
|
|
403
|
+
const summary = `${countRows(ready)} ready | ${countRows(review)} review | ${countRows(blocked)} blocked`;
|
|
404
|
+
|
|
405
|
+
ctx.ui?.setStatus?.(STATUS_KEY, summary);
|
|
406
|
+
ctx.ui?.setWidget?.(STATUS_KEY, [
|
|
407
|
+
"agile-workflow",
|
|
408
|
+
summary,
|
|
409
|
+
`root: ${substrate.root}`,
|
|
410
|
+
]);
|
|
411
|
+
|
|
412
|
+
return [
|
|
413
|
+
`agile-workflow: ${summary}`,
|
|
414
|
+
formatSection("Ready", ready),
|
|
415
|
+
formatSection("Review", review),
|
|
416
|
+
formatSection("Blocked", blocked),
|
|
417
|
+
].join("\n\n");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function show(
|
|
421
|
+
ctx: PiContext,
|
|
422
|
+
message: string,
|
|
423
|
+
level: "info" | "warning" | "error" | "success" = "info",
|
|
424
|
+
): string {
|
|
425
|
+
const clipped = truncate(message);
|
|
426
|
+
ctx.ui?.notify?.(clipped, level);
|
|
427
|
+
return clipped;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function countRows(output: string): number {
|
|
431
|
+
if (output === "(no matching items)") return 0;
|
|
432
|
+
|
|
433
|
+
return output
|
|
434
|
+
.trim()
|
|
435
|
+
.split(/\r?\n/)
|
|
436
|
+
.filter((line) => line.trim())
|
|
437
|
+
.filter((line) => !/^ID\s+KIND\s+STAGE\s+TAGS\s+PARENT/.test(line))
|
|
438
|
+
.filter((line) => !/^-+\s+-+\s+-+\s+-+\s+-+$/.test(line.trim()))
|
|
439
|
+
.length;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function formatSection(title: string, output: string): string {
|
|
443
|
+
return `## ${title}\n${output}`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function truncate(output: string): string {
|
|
447
|
+
if (output.length <= MAX_OUTPUT_CHARS) return output;
|
|
448
|
+
return `${output.slice(0, MAX_OUTPUT_CHARS)}\n\n[truncated ${output.length - MAX_OUTPUT_CHARS} chars]`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function helpText(): string {
|
|
452
|
+
return [
|
|
453
|
+
"agile-workflow commands:",
|
|
454
|
+
" /aw help Show this help",
|
|
455
|
+
" /aw status Show ready/review/blocked queue snapshot",
|
|
456
|
+
" /aw ready List ready active items",
|
|
457
|
+
" /aw blocked List blocked active items",
|
|
458
|
+
" /aw review List items at stage:review",
|
|
459
|
+
" /aw parent <id> List direct children",
|
|
460
|
+
" /aw blocking <id> List items waiting on an item",
|
|
461
|
+
" /aw board Open the agile-workflow board via the shared skill",
|
|
462
|
+
" /aw autopilot [scope] Queue the shared autopilot skill (defaults to --all)",
|
|
463
|
+
" /aw scope <idea> Queue the shared scope skill",
|
|
464
|
+
"",
|
|
465
|
+
"Long-running workflow actions are handed to shared agile-workflow skills.",
|
|
466
|
+
].join("\n");
|
|
467
|
+
}
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "agile-workflow hooks: session/compact rules context, prompt-gated principles context, and PostToolUse substrate maintenance. All hooks are flag-gated by .work/CONVENTIONS.md and use command handlers for Codex/Claude compatibility.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "startup|resume|clear|compact",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "PYTHONDONTWRITEBYTECODE=1 python3 \"${PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}/hooks/scripts/prompt-context.py\"",
|
|
11
|
+
"timeout": 5
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"UserPromptSubmit": [
|
|
17
|
+
{
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "PYTHONDONTWRITEBYTECODE=1 python3 \"${PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}/hooks/scripts/prompt-context.py\"",
|
|
22
|
+
"timeout": 10
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"PostCompact": [
|
|
28
|
+
{
|
|
29
|
+
"matcher": "manual|auto",
|
|
30
|
+
"hooks": [
|
|
31
|
+
{
|
|
32
|
+
"type": "command",
|
|
33
|
+
"command": "PYTHONDONTWRITEBYTECODE=1 python3 \"${PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}/hooks/scripts/prompt-context.py\"",
|
|
34
|
+
"timeout": 5
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"PostToolUse": [
|
|
40
|
+
{
|
|
41
|
+
"matcher": "Write|Edit|apply_patch",
|
|
42
|
+
"hooks": [
|
|
43
|
+
{
|
|
44
|
+
"type": "command",
|
|
45
|
+
"command": "PYTHONDONTWRITEBYTECODE=1 python3 \"${PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}/hooks/scripts/substrate-maintainer.py\"",
|
|
46
|
+
"timeout": 5
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|