@tenkara/afm-linux-arm64 4.0.0-alpha.7 → 4.0.0-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/bin/afm +2 -2
  2. package/content/CHANGELOG.md +28 -0
  3. package/content/PRINCIPLES.md +0 -2
  4. package/content/README-light.md +0 -2
  5. package/content/README.md +0 -2
  6. package/content/core/ach.md +0 -5
  7. package/content/core/afm.md +23 -5
  8. package/content/core/prd.md +0 -2
  9. package/content/core/protocol.md +1 -3
  10. package/content/hooks/README.md +0 -2
  11. package/content/interview/bootstrap.md +4 -2
  12. package/content/interview/promotion.md +0 -2
  13. package/content/interview/refactor.md +0 -2
  14. package/content/interview/triggers.md +0 -2
  15. package/content/modules/vireya-ui/ach.patch.md +1 -1
  16. package/content/ops/analyze.md +8 -7
  17. package/content/ops/deliver.md +23 -25
  18. package/content/ops/diagnose.md +1 -4
  19. package/content/ops/evolve.md +1 -4
  20. package/content/ops/export-template.md +1 -1
  21. package/content/ops/generalize.md +1 -4
  22. package/content/ops/policy.md +0 -2
  23. package/content/ops/reflect.md +1 -4
  24. package/content/ops/remediate.md +1 -4
  25. package/content/ops/restructure.md +1 -4
  26. package/content/ops/rule.md +0 -2
  27. package/content/ops/tasks.md +18 -18
  28. package/content/ops/update.md +3 -2
  29. package/content/ops/verify.md +0 -2
  30. package/content/questions/init.questions.yaml +13 -0
  31. package/content/rubrics/failure-classification.md +0 -2
  32. package/content/rubrics/solid-triggers.md +0 -2
  33. package/content/skills/analyze.md +1 -1
  34. package/content/skills/deliver.md +2 -2
  35. package/content/skills/tasks.md +3 -3
  36. package/content/templates/adr.template.md +1 -1
  37. package/content/templates/evolution-report.template.md +0 -2
  38. package/content/templates/feature-plan.template.md +0 -2
  39. package/content/templates/feature-research.template.md +1 -2
  40. package/content/templates/feature-spec.template.md +1 -1
  41. package/content/templates/feature-tasks.induced.template.md +58 -34
  42. package/content/templates/feature-tasks.template.md +56 -34
  43. package/content/templates/protocols/p1.md +1 -1
  44. package/content/templates/protocols/p2.md +1 -1
  45. package/content/templates/protocols/p3.md +1 -1
  46. package/package.json +1 -1
  47. package/plugin/agy/plugin.json +1 -1
  48. package/plugin/agy/skills/afm-analyze/SKILL.md +1 -1
  49. package/plugin/agy/skills/afm-deliver/SKILL.md +2 -2
  50. package/plugin/agy/skills/afm-tasks/SKILL.md +3 -3
  51. package/plugin/claude-code/.claude-plugin/plugin.json +1 -1
  52. package/plugin/claude-code/skills/analyze/SKILL.md +1 -1
  53. package/plugin/claude-code/skills/deliver/SKILL.md +2 -2
  54. package/plugin/claude-code/skills/tasks/SKILL.md +3 -3
  55. package/plugin/opencode/afm/index.js +245 -0
  56. package/plugin/opencode/afm/tui.js +21 -0
  57. package/plugin/opencode/agents/afm-executor.md +22 -14
  58. package/plugin/opencode/agents/afm-explorer.md +64 -28
  59. package/plugin/opencode/agents/afm-reviewer.md +64 -28
  60. package/plugin/opencode/skills/afm-analyze/SKILL.md +1 -1
  61. package/plugin/opencode/skills/afm-deliver/SKILL.md +2 -2
  62. package/plugin/opencode/skills/afm-tasks/SKILL.md +3 -3
  63. package/plugin/opencode/afm.js +0 -196
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: afm-tasks
3
3
  description: |
4
- Generates `tasks.md` in TDD order, and marks the parallel tasks with `[P]`. Every task references a user story or a requirement. It hard-blocks on an open `[NEEDS CLARIFICATION:]` in the plan.
4
+ Generates the `tasks:` list of `tasks.md` in TDD order with `afm task add`. Every task references a user story or a requirement. It hard-blocks on an open `[NEEDS CLARIFICATION:]` in the plan.
5
5
  Activates: `afm-tasks [<feature>]`, "generate the tasks", "break the feature into tasks".
6
6
  ---
7
7
 
@@ -19,11 +19,11 @@ Run `afm preflight` (exit 3: stop and suggest `afm-init` or `afm-refactor`). Run
19
19
 
20
20
  ## Procedure
21
21
 
22
- Break the plan into four phases (Setup, Foundation RED/GREEN, Boundary, Reconciliation) from the generator (`afm new feature-tasks --feature NNN-slug`). Mark `[P]` only where a task does not depend on the one before it; a GREEN never is. Give every task a `US-NNN` or `RF-NN`. Then run `afm feature lint <NNN>` and fix what it reports until it exits 0. The last task closes the `status:` keys. Autonomy-first: write and report.
22
+ Create the file with the generator (`afm new feature-tasks --feature NNN-slug`), then add every task with `afm task add <NNN> --kind <k> --text <t>`: setup tasks, then for each spec scenario a red task (`--scenario N`) and the green task that closes it (`--closes Txxx`), an optional refactor, the validation task of each plan boundary (`--validates <boundary name>`), and one reconcile task, last. Never write a task by hand: the tasks live in the frontmatter `tasks:` list, and the CLI assigns the id and validates the item. Mark `--parallel` only where a task does not depend on the one before it; a green never is. Give every task a `US-NNN` or `RF-NN` (`--refs`). The CLI refuses an item that breaks a lint rule. Then run `afm feature lint <NNN>` and fix what it reports until it exits 0: a missing task with `afm task add`, a wrong field or kind with `afm task set <NNN> <Txxx> --<field> <value>` (`--clear <field>` clears one), a task that should not exist with `afm task remove <NNN> <Txxx>`. Never edit the YAML by hand. The reconcile task closes the `status:` keys; close each task with `afm task done`. Autonomy-first: write and report.
23
23
 
24
24
  ## Invariant principles
25
25
 
26
- Single canonical source: `afm path PRINCIPLES.md`. Universals §1-8 apply. In particular #5 (every task traces to a US or RF) and rule 1 (TDD: no GREEN without the RED before it).
26
+ Single canonical source: `afm path PRINCIPLES.md`. Universals §1-8 apply. In particular #5 (every task traces to a US or RF) and rule 1 (TDD: no green task without the red task it closes).
27
27
 
28
28
  ## Continuous doctrine
29
29
 
@@ -1,196 +0,0 @@
1
- // AFM for opencode: forwards what the hooks decide on to `afm hook <Event>` with the native tool
2
- // name, the harness marker and the session, and carries the answer back: a deny throws, injected
3
- // context rides in the tool's output or the system prompt. A hook that cannot answer never blocks
4
- // the tool. All decisions live in the afm CLI; this file only translates opencode's plugin API.
5
- import { spawn } from "node:child_process";
6
-
7
- const MARKER = "opencode";
8
- const TIMEOUT_MS = 20000;
9
- const LOOKUP_TIMEOUT_MS = 2000;
10
- // The tools whose calls AFM decides on or records; every other call is not worth a process.
11
- const FORWARDED = new Set(["write", "edit", "multiedit", "bash", "skill"]);
12
-
13
- function afmHook(event, payload) {
14
- return new Promise((resolve) => {
15
- let out = "";
16
- const child = spawn("afm", ["hook", event], {
17
- stdio: ["pipe", "pipe", "ignore"],
18
- });
19
- const timer = setTimeout(() => {
20
- child.kill();
21
- resolve("");
22
- }, TIMEOUT_MS);
23
- child.stdout.on("data", (chunk) => {
24
- out += chunk;
25
- });
26
- child.on("error", () => {
27
- clearTimeout(timer);
28
- resolve("");
29
- });
30
- child.on("close", () => {
31
- clearTimeout(timer);
32
- resolve(out);
33
- });
34
- child.stdin.on("error", () => {});
35
- child.stdin.end(JSON.stringify(payload));
36
- });
37
- }
38
-
39
- /** The `hookSpecificOutput` of an `afm hook` answer; empty when there is none or it is not JSON. */
40
- function decisionOf(out) {
41
- try {
42
- return JSON.parse(out).hookSpecificOutput ?? {};
43
- } catch {
44
- return {};
45
- }
46
- }
47
-
48
- /** The `systemMessage` of an `afm hook` answer: what the operator must read, not the model; undefined when there is none. */
49
- function noticeOf(out) {
50
- try {
51
- const notice = JSON.parse(out).systemMessage;
52
- return typeof notice === "string" && notice !== "" ? notice : undefined;
53
- } catch {
54
- return undefined;
55
- }
56
- }
57
-
58
- /** Puts the notice on the operator's screen as a toast. The host may lack the API, and a notice is never worth a failure. */
59
- async function showNotice(client, message) {
60
- try {
61
- await client?.tui?.showToast?.({
62
- body: { title: "AFM", message, variant: "warning", duration: 20000 },
63
- });
64
- } catch {
65
- // nowhere to show it: the next session start tries again
66
- }
67
- }
68
-
69
- const bounded = (promise) => {
70
- let timer;
71
- const limit = new Promise((resolve) => {
72
- timer = setTimeout(resolve, LOOKUP_TIMEOUT_MS, undefined);
73
- });
74
- return Promise.race([promise.catch(() => undefined), limit]).finally(() =>
75
- clearTimeout(timer),
76
- );
77
- };
78
-
79
- /**
80
- * What the host says of a session: whether it is a sub-agent's (it has a parent) and the agent its
81
- * last user message names (a sub-agent session starts with the task prompt addressed to it). The
82
- * lookups are bounded and their failure is not an error: the call is then treated as the main
83
- * thread's. Only an answered lookup is remembered, so a failed one is tried again.
84
- */
85
- async function sessionOf(client, sessionID, cache) {
86
- if (typeof sessionID !== "string") return { child: false };
87
- const known = cache.get(sessionID);
88
- if (known) return known;
89
- const [info, messages] = await Promise.all([
90
- bounded(
91
- Promise.resolve(client?.session?.get?.({ path: { id: sessionID } })),
92
- ),
93
- bounded(
94
- Promise.resolve(client?.session?.messages?.({ path: { id: sessionID } })),
95
- ),
96
- ]);
97
- const users = (messages?.data ?? []).filter(
98
- (message) => message?.info?.role === "user",
99
- );
100
- const agent = users.at(-1)?.info?.agent;
101
- const session = {
102
- child: typeof info?.data?.parentID === "string",
103
- ...(typeof agent === "string" ? { agent } : {}),
104
- };
105
- if (info?.data) cache.set(sessionID, session);
106
- return session;
107
- }
108
-
109
- export const AfmHooks = async ({ directory, client }) => {
110
- const sessions = new Map();
111
- const starts = new Map();
112
- const pending = new Map();
113
- const base = (sessionID, session) => ({
114
- harness: MARKER,
115
- session_id: sessionID,
116
- cwd: directory,
117
- ...(session?.child ? { agent_id: sessionID } : {}),
118
- });
119
- // The digest a session starts with: the main thread's, or the sub-agent's rules. Asked once.
120
- const startOf = (sessionID) => {
121
- if (typeof sessionID !== "string") return Promise.resolve(null);
122
- if (!starts.has(sessionID))
123
- starts.set(
124
- sessionID,
125
- (async () => {
126
- const session = await sessionOf(client, sessionID, sessions);
127
- if (!session.child) {
128
- const out = await afmHook("SessionStart", {
129
- ...base(sessionID, session),
130
- source: "startup",
131
- });
132
- const notice = noticeOf(out);
133
- if (notice) await showNotice(client, notice);
134
- return decisionOf(out).additionalContext;
135
- }
136
- if (!session.agent) return undefined;
137
- return decisionOf(
138
- await afmHook("SubagentStart", {
139
- ...base(sessionID, session),
140
- agent_type: session.agent,
141
- }),
142
- ).additionalContext;
143
- })(),
144
- );
145
- return starts.get(sessionID);
146
- };
147
- return {
148
- "experimental.chat.system.transform": async (input, output) => {
149
- const text = await startOf(input.sessionID);
150
- if (text) output.system.push(text);
151
- },
152
- "tool.execute.before": async (input, output) => {
153
- if (!FORWARDED.has(input.tool)) return;
154
- const session = await sessionOf(client, input.sessionID, sessions);
155
- const decision = decisionOf(
156
- await afmHook("PreToolUse", {
157
- ...base(input.sessionID, session),
158
- tool_name: input.tool,
159
- tool_input: output.args ?? {},
160
- }),
161
- );
162
- if (decision.permissionDecision === "deny")
163
- throw new Error(decision.permissionDecisionReason);
164
- // A PreToolUse cannot speak to the agent before the tool runs: its context joins the output.
165
- if (decision.additionalContext && input.callID)
166
- pending.set(input.callID, decision.additionalContext);
167
- },
168
- "tool.execute.after": async (input, output) => {
169
- if (!FORWARDED.has(input.tool)) return;
170
- const session = await sessionOf(client, input.sessionID, sessions);
171
- const exit = output.metadata?.exit;
172
- const failed =
173
- input.tool === "bash" && typeof exit === "number" && exit !== 0;
174
- const decision = decisionOf(
175
- await afmHook(failed ? "PostToolUseFailure" : "PostToolUse", {
176
- ...base(input.sessionID, session),
177
- tool_name: input.tool,
178
- tool_input: input.args ?? {},
179
- ...(failed ? { exit, error: output.output ?? "" } : {}),
180
- }),
181
- );
182
- const context = [pending.get(input.callID), decision.additionalContext]
183
- .filter(Boolean)
184
- .join("\n\n");
185
- pending.delete(input.callID);
186
- if (context) output.output = `${output.output ?? ""}\n\n${context}`;
187
- },
188
- // opencode cannot hold a session open, so a Stop only records the handoff.
189
- event: async ({ event }) => {
190
- if (event?.type !== "session.idle") return;
191
- const sessionID = event.properties?.sessionID;
192
- const session = await sessionOf(client, sessionID, sessions);
193
- if (!session.child) await afmHook("Stop", base(sessionID, session));
194
- },
195
- };
196
- };