@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.
- package/bin/afm +2 -2
- package/content/CHANGELOG.md +28 -0
- package/content/PRINCIPLES.md +0 -2
- package/content/README-light.md +0 -2
- package/content/README.md +0 -2
- package/content/core/ach.md +0 -5
- package/content/core/afm.md +23 -5
- package/content/core/prd.md +0 -2
- package/content/core/protocol.md +1 -3
- package/content/hooks/README.md +0 -2
- package/content/interview/bootstrap.md +4 -2
- package/content/interview/promotion.md +0 -2
- package/content/interview/refactor.md +0 -2
- package/content/interview/triggers.md +0 -2
- package/content/modules/vireya-ui/ach.patch.md +1 -1
- package/content/ops/analyze.md +8 -7
- package/content/ops/deliver.md +23 -25
- package/content/ops/diagnose.md +1 -4
- package/content/ops/evolve.md +1 -4
- package/content/ops/export-template.md +1 -1
- package/content/ops/generalize.md +1 -4
- package/content/ops/policy.md +0 -2
- package/content/ops/reflect.md +1 -4
- package/content/ops/remediate.md +1 -4
- package/content/ops/restructure.md +1 -4
- package/content/ops/rule.md +0 -2
- package/content/ops/tasks.md +18 -18
- package/content/ops/update.md +3 -2
- package/content/ops/verify.md +0 -2
- package/content/questions/init.questions.yaml +13 -0
- package/content/rubrics/failure-classification.md +0 -2
- package/content/rubrics/solid-triggers.md +0 -2
- package/content/skills/analyze.md +1 -1
- package/content/skills/deliver.md +2 -2
- package/content/skills/tasks.md +3 -3
- package/content/templates/adr.template.md +1 -1
- package/content/templates/evolution-report.template.md +0 -2
- package/content/templates/feature-plan.template.md +0 -2
- package/content/templates/feature-research.template.md +1 -2
- package/content/templates/feature-spec.template.md +1 -1
- package/content/templates/feature-tasks.induced.template.md +58 -34
- package/content/templates/feature-tasks.template.md +56 -34
- package/content/templates/protocols/p1.md +1 -1
- package/content/templates/protocols/p2.md +1 -1
- package/content/templates/protocols/p3.md +1 -1
- package/package.json +1 -1
- package/plugin/agy/plugin.json +1 -1
- package/plugin/agy/skills/afm-analyze/SKILL.md +1 -1
- package/plugin/agy/skills/afm-deliver/SKILL.md +2 -2
- package/plugin/agy/skills/afm-tasks/SKILL.md +3 -3
- package/plugin/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugin/claude-code/skills/analyze/SKILL.md +1 -1
- package/plugin/claude-code/skills/deliver/SKILL.md +2 -2
- package/plugin/claude-code/skills/tasks/SKILL.md +3 -3
- package/plugin/opencode/afm/index.js +245 -0
- package/plugin/opencode/afm/tui.js +21 -0
- package/plugin/opencode/agents/afm-executor.md +22 -14
- package/plugin/opencode/agents/afm-explorer.md +64 -28
- package/plugin/opencode/agents/afm-reviewer.md +64 -28
- package/plugin/opencode/skills/afm-analyze/SKILL.md +1 -1
- package/plugin/opencode/skills/afm-deliver/SKILL.md +2 -2
- package/plugin/opencode/skills/afm-tasks/SKILL.md +3 -3
- package/plugin/opencode/afm.js +0 -196
|
@@ -21,7 +21,7 @@ Run `afm preflight` (exit 3: no `docs/.afm-version`, stop and suggest `afm-init`
|
|
|
21
21
|
|
|
22
22
|
## Procedure
|
|
23
23
|
|
|
24
|
-
Start from the mechanical findings: `afm feature lint <NNN> --json` covers the passes A, C, D, E and the structure of `tasks.md`. Add the judgment on top (constitution violations, duplication, the semantic side of inconsistency and ambiguity), build the coverage table, assign severities by the matrix, and report in chat with metrics. Close with the `remediation` question (`afm ask analyze`)
|
|
24
|
+
Start from the mechanical findings: `afm feature lint <NNN> --json` covers the passes A, C, D, E and the structure of `tasks.md`. Add the judgment on top (constitution violations, duplication, the semantic side of inconsistency and ambiguity), build the coverage table, assign severities by the matrix, and report in chat with metrics. Close with the `remediation` question (`afm ask analyze`); a remediation that adds a task runs `afm task add`, never a hand edit of the `tasks:` list, and a task is closed only with `afm task done`. Gating mode sorts findings into buckets instead of reporting.
|
|
25
25
|
|
|
26
26
|
## Invariant principles
|
|
27
27
|
|
|
@@ -26,10 +26,10 @@ The CLI holds the state; the skill keeps only the judgment. Drive the loop with
|
|
|
26
26
|
- **Discovery:** scan scoped to the feature, external unknowns through the nasca research flow (`nasca-research`, or `nasca-campaign` for a deep study; ask the operator if nasca is missing), unknowns sorted into resolved-by-scan, researchable and irreducible. One batched, recommendation-first round, capped by the protocol (`afm-protocol`).
|
|
27
27
|
- **Draft:** spec, plan and tasks, verified with `afm feature lint <NNN>` plus the judgment passes of `analyze`.
|
|
28
28
|
- **Gate:** the earned questions plus the `outcome` question (`afm ask deliver`), then `afm approve <NNN>`. An explicit "go" skips the questions, never the record.
|
|
29
|
-
- **Execute:**
|
|
29
|
+
- **Execute:** the tasks of the `tasks:` list run in order (`core/afm.md` § 2: a red task, the green task that closes it, an optional refactor), each checked with `afm gate --staged`, then closed with `afm task done <NNN> <Txxx>`. Tasks are added with `afm task add`, never by hand. Halt on the failure of a task that is not `parallel` after the capped retry.
|
|
30
30
|
- **Reconcile:** § 8.5 plus a passive sweep.
|
|
31
31
|
|
|
32
|
-
If artifacts already exist, resume from the correct phase, including "tasks already exist, run from the first
|
|
32
|
+
If artifacts already exist, resume from the correct phase, including "tasks already exist, run from the first task that is not done".
|
|
33
33
|
|
|
34
34
|
## Invariant principles
|
|
35
35
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: afm-tasks
|
|
3
3
|
description: |
|
|
4
|
-
Generates `tasks.md` in TDD order
|
|
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
|
-
|
|
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
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "afm",
|
|
3
3
|
"description": "Agent Flow Methodology: autonomous discovery, a strict TDD delivery loop, and documentation that evolves from measured evidence. The CLI owns state, ids, gates and lint; the skills carry the judgment.",
|
|
4
|
-
"version": "4.0.0-alpha.
|
|
4
|
+
"version": "4.0.0-alpha.8",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mapree"
|
|
7
7
|
},
|
|
@@ -21,7 +21,7 @@ Run `afm preflight` (exit 3: no `docs/.afm-version`, stop and suggest `/afm:init
|
|
|
21
21
|
|
|
22
22
|
## Procedure
|
|
23
23
|
|
|
24
|
-
Start from the mechanical findings: `afm feature lint <NNN> --json` covers the passes A, C, D, E and the structure of `tasks.md`. Add the judgment on top (constitution violations, duplication, the semantic side of inconsistency and ambiguity), build the coverage table, assign severities by the matrix, and report in chat with metrics. Close with the `remediation` question (`afm ask analyze`)
|
|
24
|
+
Start from the mechanical findings: `afm feature lint <NNN> --json` covers the passes A, C, D, E and the structure of `tasks.md`. Add the judgment on top (constitution violations, duplication, the semantic side of inconsistency and ambiguity), build the coverage table, assign severities by the matrix, and report in chat with metrics. Close with the `remediation` question (`afm ask analyze`); a remediation that adds a task runs `afm task add`, never a hand edit of the `tasks:` list, and a task is closed only with `afm task done`. Gating mode sorts findings into buckets instead of reporting.
|
|
25
25
|
|
|
26
26
|
## Invariant principles
|
|
27
27
|
|
|
@@ -26,10 +26,10 @@ The CLI holds the state; the skill keeps only the judgment. Drive the loop with
|
|
|
26
26
|
- **Discovery:** scan scoped to the feature, external unknowns through the nasca research flow (`nasca-research`, or `nasca-campaign` for a deep study; ask the operator if nasca is missing), unknowns sorted into resolved-by-scan, researchable and irreducible. One batched, recommendation-first round, capped by the protocol (`/afm:protocol`).
|
|
27
27
|
- **Draft:** spec, plan and tasks, verified with `afm feature lint <NNN>` plus the judgment passes of `analyze`.
|
|
28
28
|
- **Gate:** the earned questions plus the `outcome` question (`afm ask deliver`), then `afm approve <NNN>`. An explicit "go" skips the questions, never the record.
|
|
29
|
-
- **Execute:**
|
|
29
|
+
- **Execute:** the tasks of the `tasks:` list run in order (`core/afm.md` § 2: a red task, the green task that closes it, an optional refactor), each checked with `afm gate --staged`, then closed with `afm task done <NNN> <Txxx>`. Tasks are added with `afm task add`, never by hand. Halt on the failure of a task that is not `parallel` after the capped retry.
|
|
30
30
|
- **Reconcile:** § 8.5 plus a passive sweep.
|
|
31
31
|
|
|
32
|
-
If artifacts already exist, resume from the correct phase, including "tasks already exist, run from the first
|
|
32
|
+
If artifacts already exist, resume from the correct phase, including "tasks already exist, run from the first task that is not done".
|
|
33
33
|
|
|
34
34
|
## Invariant principles
|
|
35
35
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tasks
|
|
3
3
|
description: |
|
|
4
|
-
Generates `tasks.md` in TDD order
|
|
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`). R
|
|
|
19
19
|
|
|
20
20
|
## Procedure
|
|
21
21
|
|
|
22
|
-
|
|
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
|
|
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
|
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// AFM for opencode 2, server side: forwards what the hooks decide on to `afm hook <Event>` with the
|
|
2
|
+
// native tool name, the harness marker and the session, and carries the answer back: a deny throws,
|
|
3
|
+
// injected context rides in the tool's result or the system prompt, and a notice for the operator
|
|
4
|
+
// goes to the terminal client (tui.js) as the `notice` event of the afm RPC. A hook that cannot
|
|
5
|
+
// answer never blocks the tool. All decisions live in the afm CLI; this file only translates
|
|
6
|
+
// opencode's plugin API.
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
const MARKER = "opencode";
|
|
10
|
+
const TIMEOUT_MS = 20000;
|
|
11
|
+
const LOOKUP_TIMEOUT_MS = 2000;
|
|
12
|
+
// The tools whose calls AFM decides on or records; every other call is not worth a process.
|
|
13
|
+
const FORWARDED = new Set(["write", "edit", "shell", "skill"]);
|
|
14
|
+
// The events that end a turn: opencode cannot hold a session open, so each one only records the handoff.
|
|
15
|
+
const ENDED = new Set([
|
|
16
|
+
"session.execution.succeeded",
|
|
17
|
+
"session.execution.failed",
|
|
18
|
+
"session.execution.interrupted",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
function afmHook(event, payload) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
let out = "";
|
|
24
|
+
const child = spawn("afm", ["hook", event], {
|
|
25
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
26
|
+
});
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
child.kill();
|
|
29
|
+
resolve("");
|
|
30
|
+
}, TIMEOUT_MS);
|
|
31
|
+
child.stdout.on("data", (chunk) => {
|
|
32
|
+
out += chunk;
|
|
33
|
+
});
|
|
34
|
+
child.on("error", () => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
resolve("");
|
|
37
|
+
});
|
|
38
|
+
child.on("close", () => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
resolve(out);
|
|
41
|
+
});
|
|
42
|
+
child.stdin.on("error", () => {});
|
|
43
|
+
child.stdin.end(JSON.stringify(payload));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The `hookSpecificOutput` of an `afm hook` answer; empty when there is none or it is not JSON. */
|
|
48
|
+
function decisionOf(out) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(out).hookSpecificOutput ?? {};
|
|
51
|
+
} catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The `systemMessage` of an `afm hook` answer: what the operator must read, not the model; undefined when there is none. */
|
|
57
|
+
function noticeOf(out) {
|
|
58
|
+
try {
|
|
59
|
+
const notice = JSON.parse(out).systemMessage;
|
|
60
|
+
return typeof notice === "string" && notice !== "" ? notice : undefined;
|
|
61
|
+
} catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The afm RPC: one event, the notice the terminal client shows as a toast. Plain JSON Schema, no Effect. */
|
|
67
|
+
const RPC = {
|
|
68
|
+
id: "afm",
|
|
69
|
+
methods: {},
|
|
70
|
+
events: {
|
|
71
|
+
notice: {
|
|
72
|
+
schema: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: { message: { type: "string" } },
|
|
75
|
+
required: ["message"],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const bounded = (promise) => {
|
|
82
|
+
let timer;
|
|
83
|
+
const limit = new Promise((resolve) => {
|
|
84
|
+
timer = setTimeout(resolve, LOOKUP_TIMEOUT_MS, undefined);
|
|
85
|
+
});
|
|
86
|
+
return Promise.race([promise.catch(() => undefined), limit]).finally(() =>
|
|
87
|
+
clearTimeout(timer),
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** A tool result's content as a list of parts: the host may hand a plain string. */
|
|
92
|
+
const partsOf = (content) =>
|
|
93
|
+
typeof content === "string"
|
|
94
|
+
? [{ type: "text", text: content }]
|
|
95
|
+
: Array.isArray(content)
|
|
96
|
+
? content
|
|
97
|
+
: [];
|
|
98
|
+
|
|
99
|
+
/** The text the model reads in a tool result. */
|
|
100
|
+
const textOf = (result) =>
|
|
101
|
+
partsOf(result?.content)
|
|
102
|
+
.filter((part) => part?.type === "text")
|
|
103
|
+
.map((part) => part.text)
|
|
104
|
+
.join("\n");
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Whether a session is a sub-agent's (it has a parent). The lookup is bounded and its failure is not an
|
|
108
|
+
* error: the call is then treated as the main thread's. Only an answered lookup is remembered, so a failed
|
|
109
|
+
* one is tried again.
|
|
110
|
+
*/
|
|
111
|
+
async function isChild(session, sessionID, cache) {
|
|
112
|
+
if (typeof sessionID !== "string") return false;
|
|
113
|
+
if (cache.has(sessionID)) return cache.get(sessionID);
|
|
114
|
+
const info = await bounded(
|
|
115
|
+
Promise.resolve().then(() => session?.get?.({ sessionID })),
|
|
116
|
+
);
|
|
117
|
+
if (info === undefined || info === null) return false;
|
|
118
|
+
const child = typeof info.parentID === "string";
|
|
119
|
+
cache.set(sessionID, child);
|
|
120
|
+
return child;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export default {
|
|
124
|
+
id: "afm",
|
|
125
|
+
async setup(ctx) {
|
|
126
|
+
const directory = ctx.location?.directory;
|
|
127
|
+
// A host whose RPC is missing or refuses the definition still runs the hooks: only the toast is lost.
|
|
128
|
+
const rpc = await Promise.resolve()
|
|
129
|
+
.then(() => ctx.rpc?.register?.(RPC, {}))
|
|
130
|
+
.catch(() => undefined);
|
|
131
|
+
const notify = async (message) => {
|
|
132
|
+
try {
|
|
133
|
+
await rpc?.events?.emit?.("notice", { message });
|
|
134
|
+
} catch {
|
|
135
|
+
// nowhere to show it: the next session start tries again
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const children = new Map();
|
|
139
|
+
const starts = new Map();
|
|
140
|
+
const pending = new Map();
|
|
141
|
+
const childOf = (sessionID) => isChild(ctx.session, sessionID, children);
|
|
142
|
+
const base = (sessionID, child) => ({
|
|
143
|
+
harness: MARKER,
|
|
144
|
+
session_id: sessionID,
|
|
145
|
+
cwd: directory,
|
|
146
|
+
...(child ? { agent_id: sessionID } : {}),
|
|
147
|
+
});
|
|
148
|
+
// The digest a session starts with: the main thread's, or the sub-agent's rules. Asked once.
|
|
149
|
+
const startOf = (sessionID, agent) => {
|
|
150
|
+
if (typeof sessionID !== "string") return Promise.resolve(undefined);
|
|
151
|
+
if (!starts.has(sessionID))
|
|
152
|
+
starts.set(
|
|
153
|
+
sessionID,
|
|
154
|
+
(async () => {
|
|
155
|
+
const child = await childOf(sessionID);
|
|
156
|
+
if (!child) {
|
|
157
|
+
const out = await afmHook("SessionStart", {
|
|
158
|
+
...base(sessionID, false),
|
|
159
|
+
source: "startup",
|
|
160
|
+
});
|
|
161
|
+
const notice = noticeOf(out);
|
|
162
|
+
if (notice) await notify(notice);
|
|
163
|
+
return decisionOf(out).additionalContext;
|
|
164
|
+
}
|
|
165
|
+
if (typeof agent !== "string") return undefined;
|
|
166
|
+
return decisionOf(
|
|
167
|
+
await afmHook("SubagentStart", {
|
|
168
|
+
...base(sessionID, true),
|
|
169
|
+
agent_type: agent,
|
|
170
|
+
}),
|
|
171
|
+
).additionalContext;
|
|
172
|
+
})(),
|
|
173
|
+
);
|
|
174
|
+
return starts.get(sessionID);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Runs before every model request: the system prompt is rebuilt each time, so the digest is added each time.
|
|
178
|
+
await ctx.session.hook("context", async (event) => {
|
|
179
|
+
const text = await startOf(event.sessionID, event.agent);
|
|
180
|
+
if (text) event.system.push({ type: "text", text });
|
|
181
|
+
});
|
|
182
|
+
await ctx.tool.hook("execute.before", async (event) => {
|
|
183
|
+
if (!FORWARDED.has(event.tool)) return;
|
|
184
|
+
const child = await childOf(event.sessionID);
|
|
185
|
+
const decision = decisionOf(
|
|
186
|
+
await afmHook("PreToolUse", {
|
|
187
|
+
...base(event.sessionID, child),
|
|
188
|
+
tool_name: event.tool,
|
|
189
|
+
tool_input: event.input ?? {},
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
if (decision.permissionDecision === "deny")
|
|
193
|
+
throw new Error(decision.permissionDecisionReason);
|
|
194
|
+
// A PreToolUse cannot speak to the agent before the tool runs: its context joins the result.
|
|
195
|
+
if (decision.additionalContext && event.id)
|
|
196
|
+
pending.set(event.id, decision.additionalContext);
|
|
197
|
+
});
|
|
198
|
+
await ctx.tool.hook("execute.after", async (event) => {
|
|
199
|
+
if (!FORWARDED.has(event.tool)) return;
|
|
200
|
+
const child = await childOf(event.sessionID);
|
|
201
|
+
const exit =
|
|
202
|
+
event.status === "completed" ? event.result?.metadata?.exit : undefined;
|
|
203
|
+
const failed =
|
|
204
|
+
event.tool === "shell" && typeof exit === "number" && exit !== 0;
|
|
205
|
+
const decision = decisionOf(
|
|
206
|
+
await afmHook(failed ? "PostToolUseFailure" : "PostToolUse", {
|
|
207
|
+
...base(event.sessionID, child),
|
|
208
|
+
tool_name: event.tool,
|
|
209
|
+
tool_input: event.input ?? {},
|
|
210
|
+
...(failed ? { exit, error: textOf(event.result) } : {}),
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
const context = [pending.get(event.id), decision.additionalContext]
|
|
214
|
+
.filter(Boolean)
|
|
215
|
+
.join("\n\n");
|
|
216
|
+
pending.delete(event.id);
|
|
217
|
+
if (context && event.status === "completed")
|
|
218
|
+
event.result = {
|
|
219
|
+
...event.result,
|
|
220
|
+
content: [
|
|
221
|
+
...partsOf(event.result?.content),
|
|
222
|
+
{ type: "text", text: context },
|
|
223
|
+
],
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const controller = new AbortController();
|
|
228
|
+
(async () => {
|
|
229
|
+
for await (const event of ctx.event.subscribe({
|
|
230
|
+
signal: controller.signal,
|
|
231
|
+
})) {
|
|
232
|
+
if (!ENDED.has(event?.type)) continue;
|
|
233
|
+
const sessionID = event.data?.sessionID;
|
|
234
|
+
if (!(await childOf(sessionID)))
|
|
235
|
+
await afmHook("Stop", base(sessionID, false));
|
|
236
|
+
}
|
|
237
|
+
})().catch(() => {
|
|
238
|
+
// the stream ends with the plugin: nothing is left to record
|
|
239
|
+
});
|
|
240
|
+
return () => {
|
|
241
|
+
controller.abort();
|
|
242
|
+
return rpc?.dispose?.();
|
|
243
|
+
};
|
|
244
|
+
},
|
|
245
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// AFM for opencode 2, terminal-client side: shows the notice the server side (index.js) emits on the
|
|
2
|
+
// afm RPC as a toast. opencode loads this file in the terminal client only; a server plugin has no screen.
|
|
3
|
+
|
|
4
|
+
/** How long a notice stays: long enough to read a sentence about a failed update. */
|
|
5
|
+
const DURATION_MS = 20000;
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
id: "afm",
|
|
9
|
+
setup(ctx) {
|
|
10
|
+
return ctx.data.on("rpc.afm.notice", (event) => {
|
|
11
|
+
const message = event?.data?.message;
|
|
12
|
+
if (typeof message !== "string" || message === "") return;
|
|
13
|
+
ctx.ui.toast.show({
|
|
14
|
+
title: "AFM",
|
|
15
|
+
message,
|
|
16
|
+
variant: "warning",
|
|
17
|
+
duration: DURATION_MS,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Writer of one bounded change. Makes the edit its brief names, test first, inside the files the brief grants, and reports what changed; dispatched with a brief.
|
|
3
3
|
mode: subagent
|
|
4
|
-
|
|
5
|
-
"*"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
4
|
+
permissions:
|
|
5
|
+
- action: "*"
|
|
6
|
+
resource: "*"
|
|
7
|
+
effect: deny
|
|
8
|
+
- action: "read"
|
|
9
|
+
resource: "*"
|
|
10
|
+
effect: allow
|
|
11
|
+
- action: "glob"
|
|
12
|
+
resource: "*"
|
|
13
|
+
effect: allow
|
|
14
|
+
- action: "grep"
|
|
15
|
+
resource: "*"
|
|
16
|
+
effect: allow
|
|
17
|
+
- action: "edit"
|
|
18
|
+
resource: "*"
|
|
19
|
+
effect: allow
|
|
20
|
+
- action: "shell"
|
|
21
|
+
resource: "*"
|
|
22
|
+
effect: allow
|
|
23
|
+
- action: "afm_*"
|
|
24
|
+
resource: "*"
|
|
25
|
+
effect: allow
|
|
18
26
|
---
|
|
19
27
|
You are an executor. The message that dispatched you is your brief: follow it.
|
|
20
28
|
|
|
@@ -2,34 +2,70 @@
|
|
|
2
2
|
description: Read-only investigator. Answers one question from the code, the docs or the web, cites what it read, and changes nothing; dispatched with a brief.
|
|
3
3
|
mode: subagent
|
|
4
4
|
steps: 60
|
|
5
|
-
|
|
6
|
-
"*"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
5
|
+
permissions:
|
|
6
|
+
- action: "*"
|
|
7
|
+
resource: "*"
|
|
8
|
+
effect: deny
|
|
9
|
+
- action: "read"
|
|
10
|
+
resource: "*"
|
|
11
|
+
effect: allow
|
|
12
|
+
- action: "glob"
|
|
13
|
+
resource: "*"
|
|
14
|
+
effect: allow
|
|
15
|
+
- action: "grep"
|
|
16
|
+
resource: "*"
|
|
17
|
+
effect: allow
|
|
18
|
+
- action: "webfetch"
|
|
19
|
+
resource: "*"
|
|
20
|
+
effect: allow
|
|
21
|
+
- action: "websearch"
|
|
22
|
+
resource: "*"
|
|
23
|
+
effect: allow
|
|
24
|
+
- action: "afm_approve"
|
|
25
|
+
resource: "*"
|
|
26
|
+
effect: allow
|
|
27
|
+
- action: "afm_config"
|
|
28
|
+
resource: "*"
|
|
29
|
+
effect: allow
|
|
30
|
+
- action: "afm_detect"
|
|
31
|
+
resource: "*"
|
|
32
|
+
effect: allow
|
|
33
|
+
- action: "afm_doctor"
|
|
34
|
+
resource: "*"
|
|
35
|
+
effect: allow
|
|
36
|
+
- action: "afm_failures_promotable"
|
|
37
|
+
resource: "*"
|
|
38
|
+
effect: allow
|
|
39
|
+
- action: "afm_feature_lint"
|
|
40
|
+
resource: "*"
|
|
41
|
+
effect: allow
|
|
42
|
+
- action: "afm_feature_status"
|
|
43
|
+
resource: "*"
|
|
44
|
+
effect: allow
|
|
45
|
+
- action: "afm_gate"
|
|
46
|
+
resource: "*"
|
|
47
|
+
effect: allow
|
|
48
|
+
- action: "afm_index"
|
|
49
|
+
resource: "*"
|
|
50
|
+
effect: allow
|
|
51
|
+
- action: "afm_lang"
|
|
52
|
+
resource: "*"
|
|
53
|
+
effect: allow
|
|
54
|
+
- action: "afm_next_id"
|
|
55
|
+
resource: "*"
|
|
56
|
+
effect: allow
|
|
57
|
+
- action: "afm_path"
|
|
58
|
+
resource: "*"
|
|
59
|
+
effect: allow
|
|
60
|
+
- action: "afm_policy"
|
|
61
|
+
resource: "*"
|
|
62
|
+
effect: allow
|
|
63
|
+
- action: "afm_preflight"
|
|
64
|
+
resource: "*"
|
|
65
|
+
effect: allow
|
|
66
|
+
- action: "afm_version"
|
|
67
|
+
resource: "*"
|
|
68
|
+
effect: allow
|
|
33
69
|
---
|
|
34
70
|
You are an explorer. The message that dispatched you is your brief: follow it.
|
|
35
71
|
|
|
@@ -1,34 +1,70 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Blind auditor. Checks one piece of work it did not write against its sources and rules, and returns a verdict with evidence; dispatched with a brief.
|
|
3
3
|
mode: subagent
|
|
4
|
-
|
|
5
|
-
"*"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
4
|
+
permissions:
|
|
5
|
+
- action: "*"
|
|
6
|
+
resource: "*"
|
|
7
|
+
effect: deny
|
|
8
|
+
- action: "read"
|
|
9
|
+
resource: "*"
|
|
10
|
+
effect: allow
|
|
11
|
+
- action: "glob"
|
|
12
|
+
resource: "*"
|
|
13
|
+
effect: allow
|
|
14
|
+
- action: "grep"
|
|
15
|
+
resource: "*"
|
|
16
|
+
effect: allow
|
|
17
|
+
- action: "webfetch"
|
|
18
|
+
resource: "*"
|
|
19
|
+
effect: allow
|
|
20
|
+
- action: "websearch"
|
|
21
|
+
resource: "*"
|
|
22
|
+
effect: allow
|
|
23
|
+
- action: "afm_approve"
|
|
24
|
+
resource: "*"
|
|
25
|
+
effect: allow
|
|
26
|
+
- action: "afm_config"
|
|
27
|
+
resource: "*"
|
|
28
|
+
effect: allow
|
|
29
|
+
- action: "afm_detect"
|
|
30
|
+
resource: "*"
|
|
31
|
+
effect: allow
|
|
32
|
+
- action: "afm_doctor"
|
|
33
|
+
resource: "*"
|
|
34
|
+
effect: allow
|
|
35
|
+
- action: "afm_failures_promotable"
|
|
36
|
+
resource: "*"
|
|
37
|
+
effect: allow
|
|
38
|
+
- action: "afm_feature_lint"
|
|
39
|
+
resource: "*"
|
|
40
|
+
effect: allow
|
|
41
|
+
- action: "afm_feature_status"
|
|
42
|
+
resource: "*"
|
|
43
|
+
effect: allow
|
|
44
|
+
- action: "afm_gate"
|
|
45
|
+
resource: "*"
|
|
46
|
+
effect: allow
|
|
47
|
+
- action: "afm_index"
|
|
48
|
+
resource: "*"
|
|
49
|
+
effect: allow
|
|
50
|
+
- action: "afm_lang"
|
|
51
|
+
resource: "*"
|
|
52
|
+
effect: allow
|
|
53
|
+
- action: "afm_next_id"
|
|
54
|
+
resource: "*"
|
|
55
|
+
effect: allow
|
|
56
|
+
- action: "afm_path"
|
|
57
|
+
resource: "*"
|
|
58
|
+
effect: allow
|
|
59
|
+
- action: "afm_policy"
|
|
60
|
+
resource: "*"
|
|
61
|
+
effect: allow
|
|
62
|
+
- action: "afm_preflight"
|
|
63
|
+
resource: "*"
|
|
64
|
+
effect: allow
|
|
65
|
+
- action: "afm_version"
|
|
66
|
+
resource: "*"
|
|
67
|
+
effect: allow
|
|
32
68
|
---
|
|
33
69
|
You are a reviewer. The message that dispatched you is your brief: follow it.
|
|
34
70
|
|
|
@@ -21,7 +21,7 @@ Run `afm preflight` (exit 3: no `docs/.afm-version`, stop and suggest `afm-init`
|
|
|
21
21
|
|
|
22
22
|
## Procedure
|
|
23
23
|
|
|
24
|
-
Start from the mechanical findings: `afm feature lint <NNN> --json` covers the passes A, C, D, E and the structure of `tasks.md`. Add the judgment on top (constitution violations, duplication, the semantic side of inconsistency and ambiguity), build the coverage table, assign severities by the matrix, and report in chat with metrics. Close with the `remediation` question (`afm ask analyze`)
|
|
24
|
+
Start from the mechanical findings: `afm feature lint <NNN> --json` covers the passes A, C, D, E and the structure of `tasks.md`. Add the judgment on top (constitution violations, duplication, the semantic side of inconsistency and ambiguity), build the coverage table, assign severities by the matrix, and report in chat with metrics. Close with the `remediation` question (`afm ask analyze`); a remediation that adds a task runs `afm task add`, never a hand edit of the `tasks:` list, and a task is closed only with `afm task done`. Gating mode sorts findings into buckets instead of reporting.
|
|
25
25
|
|
|
26
26
|
## Invariant principles
|
|
27
27
|
|
|
@@ -26,10 +26,10 @@ The CLI holds the state; the skill keeps only the judgment. Drive the loop with
|
|
|
26
26
|
- **Discovery:** scan scoped to the feature, external unknowns through the nasca research flow (`nasca-research`, or `nasca-campaign` for a deep study; ask the operator if nasca is missing), unknowns sorted into resolved-by-scan, researchable and irreducible. One batched, recommendation-first round, capped by the protocol (`afm-protocol`).
|
|
27
27
|
- **Draft:** spec, plan and tasks, verified with `afm feature lint <NNN>` plus the judgment passes of `analyze`.
|
|
28
28
|
- **Gate:** the earned questions plus the `outcome` question (`afm ask deliver`), then `afm approve <NNN>`. An explicit "go" skips the questions, never the record.
|
|
29
|
-
- **Execute:**
|
|
29
|
+
- **Execute:** the tasks of the `tasks:` list run in order (`core/afm.md` § 2: a red task, the green task that closes it, an optional refactor), each checked with `afm gate --staged`, then closed with `afm task done <NNN> <Txxx>`. Tasks are added with `afm task add`, never by hand. Halt on the failure of a task that is not `parallel` after the capped retry.
|
|
30
30
|
- **Reconcile:** § 8.5 plus a passive sweep.
|
|
31
31
|
|
|
32
|
-
If artifacts already exist, resume from the correct phase, including "tasks already exist, run from the first
|
|
32
|
+
If artifacts already exist, resume from the correct phase, including "tasks already exist, run from the first task that is not done".
|
|
33
33
|
|
|
34
34
|
## Invariant principles
|
|
35
35
|
|