@sjawhar/opencode-legion-envoy 3.13.0 → 3.15.0

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.
@@ -0,0 +1,101 @@
1
+ # The merge gate: review, retro, READY, and the production check
2
+
3
+ Part of `skill://legion-worker`. Read it when you are the reviewer submitting a review or an
4
+ approval, the implementer pushing the `.legion/` deletion or recording the production check, or
5
+ the merger publishing READY. Every path it cites is in sjawhar/legion.
6
+
7
+ The order, in full: the tester's evidence green → the implementer's `.legion/` deletion push →
8
+ the reviewer's approval of that head → retro → the merger's READY → the human merge → the
9
+ implementer's production check. After the approval, only retro's `docs/solutions/` commit leaves
10
+ it standing on its own (*Retro*, below). A conflict-forced merge goes back to the reviewer for a
11
+ confirmation or a new round, as the fingerprint decides (*The reviewer*, below, and
12
+ `skill://legion-worker/references/conflicts-and-rewrites.md`), and any other change voids it.
13
+
14
+ ## The reviewer
15
+
16
+ - The reviewer verifies the `CI`, `Threads`, and `E2E` facts against GitHub directly —
17
+ never from a handoff — then runs `task(agent="thermonuclear-deep-review")` and
18
+ `task(agent="thermonuclear-code-quality")` once at that head — the head the implementer's
19
+ simplify pass left final — and records the verdict.
20
+ Approval is refused while either `E2E (implementer)` or `E2E (tester)` is missing: `REQUEST_CHANGES` naming the missing line.
21
+ Skip the `Thermo` line entirely on a docs-only PR. Submit **one review per round** —
22
+ `REQUEST_CHANGES` when any correctness finding stands, otherwise `COMMENT` while the head
23
+ still carries `.legion/`; `APPROVE` only for a head that carries no `.legion/` — the head
24
+ that differs from the reviewed one by the `.legion/` deletion alone, or, after a
25
+ conflict-forced rebase, the new head whose fingerprint equals the approved head's — always
26
+ named by SHA — carrying every inline comment in that single
27
+ call: `legion gh -- api --method POST repos/{owner}/{repo}/pulls/{number}/reviews --input body.json`
28
+ with `commit_id`, `event` (`REQUEST_CHANGES`, `COMMENT`, or `APPROVE`), `body` (with the
29
+ Legion footer), and a `comments[]` array of `{path, line, side, body}`, one entry per
30
+ finding — never one `pr review` call per finding (each submission fires a `pr-review` wake).
31
+ Then return the issue to the architect; when clean, have the architect send the implementer
32
+ back to push the `.legion/` deletion, then review **that** head and approve it by name. After a
33
+ conflict-forced rebase, compute the fingerprint (*The unchanged-diff check* in
34
+ `skill://legion-worker/references/conflicts-and-rewrites.md`) at the
35
+ `commit_id` of your last submitted review and at the new head. Equal and that review was
36
+ `APPROVE`: submit one more `APPROVE` naming the new head by SHA, its body naming both SHAs
37
+ and the fingerprint — a confirmation, not a round; no thermo pass, no thread pass. Equal and
38
+ that review was `COMMENT` or `REQUEST_CHANGES`: continue that round against the new head;
39
+ nothing restarts. Different: a new round — thermo again, one review.
40
+ - Answer every thread you opened, and every thread a bot opened that is none of Legion's role
41
+ Apps, as `skill://legion-worker/references/review-threads.md` says; the same reference says
42
+ when every thread is settled enough to approve.
43
+
44
+ A reviewer's phase ends with its completion, not with its review. A round that writes a handoff
45
+ takes this order: write, commit and push the handoff; submit the review of the head that push
46
+ made, by its SHA; then complete. An approval waits for the CI verdict to settle green at that head
47
+ before you submit it, since an approval stands only on green checks and GitHub can dismiss one
48
+ once the head moves, and a verdict that settles red there makes the round's decision a request for
49
+ changes naming the failing checks; a request for changes does not wait, since it stands whatever CI says and the
50
+ issue leaves reviewing with it. A review of a head the handoff push then replaces names a head
51
+ the pull request no longer has. A round that writes none (the final approval of the `.legion/`
52
+ deletion head) reviews the head as it is. The daemon moves the issue once both are in —
53
+ the decision GitHub reports and your completion, in either order — so a review posted without a
54
+ completion leaves the issue in reviewing until you finish.
55
+
56
+ ## Retro
57
+
58
+ - **Retro's commit does not void the reviewer's approval.** After the reviewer approves the
59
+ cleaned head, retro commits its learnings under `docs/solutions/` on top of it; that commit
60
+ stays, the approval stands, and the tree goes to the merger — never back to the tester or
61
+ reviewer. Anything else above the approved head does void it, and the merger tells the
62
+ architect the head must return to review instead of publishing. A conflict-forced rebase
63
+ after retro moves those documents with the branch; retro never re-runs.
64
+
65
+ ## The merger
66
+
67
+ - The merger runs `legion threads resolve --pr <n> --repo <owner>/<repo>` (it acts as the same
68
+ code-writing App as the implementer; resolving a thread changes no commit, so this run never
69
+ invalidates the approval), does not publish while any `left open` line remains or the command
70
+ exits 1 (report the thread to the architect instead), then proves that rule with two commands.
71
+ First `cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && jj -R
72
+ "$LEGION_WORKSPACE" diff --from <approved-sha> --to <tip-sha> --summary`, whose output is quoted
73
+ in READY (an empty output is quoted as `no file changes above the approved head`); then the same
74
+ with `'~docs/solutions'` appended, which must print nothing. The merger always posts
75
+ `READY #<n> at <current sha> (approved at <approved sha>) for <KEY> (<pr url>)` (the shape
76
+ `packages/pi-envoy/roles/merger.md` defines), its summary, and the PR body's gate facts as a
77
+ `dispatch_message` on the issue. When the `Legion addressing` line names a merge queue, it also
78
+ publishes the same packet there with `envoy_publish`; a 404 means the Dispatch message remains
79
+ the durable notice and the merger stays idle. The READY packet names both the implementer's and
80
+ tester's `E2E` lines; a missing one is reported to the architect instead of published. Legion
81
+ never merges.
82
+
83
+ ## After the human merge
84
+
85
+ - **After a human merges, the implementer verifies in production.** Sami, 2026-09-13,
86
+ verbatim: "the agent that developed it should be responsible for testing in production."
87
+ The architect sends the implementer back once the merge lands; the implementer watches the
88
+ deploy slot that carries the merge to `production-apply` (or the equivalent publish step),
89
+ drives the changed path in production through the user's own access path, and records the
90
+ observation on the PR and the issue before the architect signs off. A staging pass is not
91
+ this: on 2026-09-12 a slot's entire staging gate passed at 00:02Z and its production-apply
92
+ failed at 00:12Z on a resource staging never runs. If the slot fails on the change, the
93
+ implementer owns the fix and the next slot.
94
+ The record has three places: the PR body's `Production:` line, one pull-request comment
95
+ carrying the Legion footer, and a `dispatch_message` on the issue — the reviewer and merger
96
+ read GitHub, the architect reads the issue. When the deploy that carries the merge has not
97
+ happened (a shared profile still holding the previous plugin release, a daemon still running
98
+ the previous commit, a slot nobody has run), open a `dispatch_ask` naming the exact install or
99
+ restart step, with options for its outcomes, keep the `Production:` line at `pending <what is
100
+ missing>`, and complete the check once the human answers that it is done. Never record a
101
+ staging pass as the production check, and never let the architect sign off on a `pending` line.
@@ -0,0 +1,125 @@
1
+ # PR body, proofs, and the simplify pass
2
+
3
+ Part of `skill://legion-worker`. Read it before you write or edit any line of the pull request
4
+ body, put a `proof` array in a handoff, verify another phase's proof, or run the simplify pass.
5
+ Every path it cites is in sjawhar/legion.
6
+
7
+ ## The READY format
8
+
9
+ The implementer writes the PR body in the READY format from the moment the PR opens, and every
10
+ later phase keeps it current rather than replacing it:
11
+
12
+ ```
13
+ ## Verification
14
+
15
+ **CI:** `Tests` run <run-id> — jobs lint, typecheck, test all success at <head-sha>; `PR Title` run <run-id> — job pr-title success at <head-sha>.
16
+
17
+ **Threads:** <n> resolved, 0 unresolved. Each disposed individually, never in bulk:
18
+ - Thread <id>: fixed in <commit-sha> — <one line>.
19
+ - Thread <id>: not a defect — <reason>.
20
+ `legion threads resolve --pr <n> --repo <owner>/<repo>` at <head-sha>:
21
+ resolved <thread URL> — its opener's acceptance
22
+ resolved <thread URL> — the Legion reviewer's acceptance of a bot's thread
23
+ left open <thread URL> — newest reply by <login> is not an acceptance
24
+ left open <thread URL> — newest reply by <login> is an unsubmitted draft in a pending review
25
+ left open <thread URL> — newest reply by <login> is not its opener's or the Legion reviewer's acceptance
26
+
27
+ **Thermo:** `ce-simplify-code` once at <head-sha>: <0 applied | applied → new head <sha>>; thermonuclear pair at the final head <sha>:
28
+ <verdict>. (omitted entirely on a docs-only PR — there is no code for either pass, so neither runs)
29
+
30
+ **E2E (implementer):** <surface> — ran `<command or run id>`, observed <result>, at head <sha>.
31
+ Negative control: <deliberately broken input> → <refusal or failure observed>.
32
+
33
+ **E2E (tester):** <surface> — ran `<command or run id>`, observed <result>, at head <sha>.
34
+ Negative control: <deliberately broken input> → <refusal or failure observed>.
35
+ Verified the implementer's proof by <re-running its command | driving the same surface independently>.
36
+
37
+ **Production:** <what was checked in production, how, what was observed> — merge commit <sha>.
38
+ (written by the implementer after the merge lands; `pending <what is missing>` until then)
39
+
40
+ **Fast-follow:** <one named cleanup item and where it will land>, or "none".
41
+
42
+ **Chain:** stacked on <base bookmark> frozen at <sha> / not stacked.
43
+ ```
44
+
45
+ ## What a proof is
46
+
47
+ **A proof** is the changed behaviour exercised on the surface a user reaches it through, recorded
48
+ as the exact command or run id, what was observed, the head SHA, and one negative control —
49
+ a deliberately broken input and the refusal or failure observed. The surface is
50
+ **production-like** — the repository's real-process test harness and fixtures, a sandbox
51
+ repository, a real browser, a devN stack, staging, or a local stack with real migrations, one that
52
+ has the resource the change touches — and each `E2E` line carries a **link** to that run,
53
+ screenshot, or e2e; human review does not replace user-facing verification, and a green unit suite
54
+ is not it. A unit or integration test is a regression lock, never proof of a criterion. Sami,
55
+ 2026-09-13, verbatim: "They need to test everything in a production-like
56
+ environment before merging, and it is the agent that develops the feature that is responsible
57
+ for doing that. If there's anything blocking that, we need to fix it: if it's infrastructure, we
58
+ need to fix it; if it's tooling, we need to develop it; if it's skills, we need to fix the skills
59
+ ... it should not require deploying to production to realize your feature doesn't work."
60
+ Evidence for the rule: in the week of 2026-09-08 three surfaces merged green and were wrong on
61
+ inspection (the Astrolabe IPI stack, Dispatch on ECS, the candidate flow), and on 2026-09-12 six
62
+ deploy slots died on code first executed after merge, including a production-only ECS bootstrap
63
+ the whole staging gate never ran. The implementer's proof and the tester's proof below are both
64
+ this proof.
65
+
66
+ ## The rules every phase's evidence follows
67
+
68
+ - **The implementer proves the change before its phase completes, and writes the `E2E (implementer)` line when the pull request opens.**
69
+ The proof is the one defined above. It goes into `.legion/implement.json` as the required `proof`
70
+ array (`handoff_write` for phase `implement` refuses a payload without one, or with a blank or
71
+ whitespace-only field, and names the field), and into the PR body, because the reviewer and the
72
+ merger verify facts on GitHub and never from a handoff.
73
+ - **The tester verifies the implementer's proof and adds its own `E2E (tester)` line.** It re-runs
74
+ the implementer's command or drives the same surface independently, and records the verdict in
75
+ `.legion/test.json` as `implementerProof` (`{verdict, how}`).
76
+ A test handoff whose predecessor carried no proof is a test failure, not a gap for the tester to fill:
77
+ record it in `failures` with `implementerProof.verdict: "rejected"`, complete the phase, and let
78
+ the architect return the issue to the implementer — the agent that developed the change owns
79
+ proving it (`handoff_write` for phase `test` refuses a rejected verdict, or `failed > 0`,
80
+ with no recorded failure). Otherwise, add your own proof before completing — a proof as defined
81
+ above — as the `E2E (tester)` line and the `proof` array `handoff_write` for
82
+ phase `test` requires whenever you report no failure. A code path whose first execution is after merge — a
83
+ deploy workflow's inline step, a post-merge helper, a production-only resource — is untested
84
+ until the implementer has executed it against a devN stack; if no surface can reach it, the
85
+ tester names that missing surface as the blocker instead of passing the phase. Environment or
86
+ secret-scrub evidence (e.g. "`LEGION_*`/`DISPATCH_*`/`ENVOY_*` unset") is recorded once, in
87
+ `.legion/test.json`, and only when the issue's acceptance criteria call for it — never
88
+ re-pasted into the PR body each round. After a conflict-forced rebase, compute the
89
+ fingerprint (*The unchanged-diff check* in
90
+ `skill://legion-worker/references/conflicts-and-rewrites.md`) at the head your `E2E` line
91
+ names and at the new head. Equal: re-run only the
92
+ bare gates — the repository's CI green at the new head and its smoke check — and change the
93
+ `E2E` line's head to the new SHA with
94
+ `rebase re-check <old-sha> → <new-sha>: fingerprint unchanged, bare gates only`; the
95
+ real-surface verification is not repeated. Different: a full test round.
96
+ - **The implementer runs `skill://ce-simplify-code` once per pull request, after the last review round
97
+ closes and before the reviewer's final pass, when the diff touches runtime code; a docs-only
98
+ diff gets none.** It is scoped to the pull request's own diff, at the head where the last review
99
+ round closed: nothing applied leaves that head final; applied → the applied head is the final
100
+ head: CI runs on it, the pair runs once on it, and the E2E proof re-runs on it for the surface
101
+ the simplify diff touched (Sami, 2026-09-13: test on the real surface before merging, no
102
+ shortcuts — a refactor that "preserves behaviour" is a claim until it is executed). That cost is
103
+ why 0-applied is the expected outcome and a pass that applies is spent sparingly. At the applied
104
+ head the implementer re-cites the `CI` line and re-runs its own proof into `E2E (implementer)`,
105
+ and the tester re-runs its proof for the touched surface into `E2E (tester)`, before the
106
+ reviewer's final pass. Simplify is the last code change; the pair is the last review. Record it
107
+ in the `Thermo` line.
108
+ - **No deferrals** is the body's rule (*PR body, review, and the merge gate* in
109
+ `skill://legion-worker`): the `Fast-follow:` line holds naming, duplication, or wording cleanup
110
+ only. A base frozen for others to stack on is never rewritten (*Rewriting pushed commits* in
111
+ `skill://legion-worker/references/conflicts-and-rewrites.md`); the `Chain` line records it.
112
+
113
+ ## When no surface reaches the changed path
114
+
115
+ No surface reaches the changed path is a report to the architect, never a reason to complete the phase.
116
+ Say which surface is missing and what it would have to do — a rig that can spawn the role, a
117
+ sandbox that holds the resource, a credential, a command that does not exist yet — and send it to
118
+ the architect with `envoy_publish` to its role topic. The architect creates a child issue in this
119
+ tree to build it (infrastructure, tooling, or a skill) and resumes you once it lands. Sami,
120
+ 2026-09-13, verbatim: "If there's anything blocking that, we need to fix it: if it's
121
+ infrastructure, we need to fix it; if it's tooling, we need to develop it; if it's skills, we need
122
+ to fix the skills." A code path whose first execution would be after the merge — a deploy
123
+ workflow's inline step, a post-merge helper, a production-only resource — is untested until you
124
+ have executed it somewhere production-like; completing with a unit-test-only handoff is the
125
+ failure this rule exists to stop.
@@ -0,0 +1,71 @@
1
+ # Review threads
2
+
3
+ Part of `skill://legion-worker`. Read it when you reply to, accept, or resolve a review thread,
4
+ or run `legion threads resolve`: the implementer before every push that answers a review, the
5
+ reviewer on every re-review, the merger before READY. Every path it cites is in sjawhar/legion.
6
+
7
+ - **Threads are dispositioned individually, never resolved in bulk.** Every open review
8
+ thread gets its own line naming the fixing commit or the reason it isn't a defect. The
9
+ reviewer answers each thread it opened, and each thread a bot opened that is none of Legion's
10
+ role Apps, with exactly one of `Accepted: fixed in <commit> — <one line>`,
11
+ `Accepted: not a defect — <reason>`, or `Still open: <what remains>`; nothing else is an
12
+ acceptance, and nobody replies after an `Accepted:` (any later reply that is not itself an
13
+ `Accepted:` — the opener's own follow-up included — leaves the thread open, because resolution
14
+ considers only the newest comment). The review App can reply on a thread but cannot resolve it:
15
+ GitHub grants resolving a review thread to the pull request's author, and the implementer opens
16
+ every Legion pull request (`packages/daemon/src/daemon/AGENTS.md`, GitHub Apps).
17
+ When `LEGION_GRANT_FILE` or `LEGION_GRANT` is set, use `legion threads resolve --pr <number> --repo <owner>/<repo>`.
18
+ When neither is set, add `--gh` to that command, which applies the fallback's rule below through
19
+ your own `gh`; where no `legion` command is installed, use `gh api graphql` with the session's
20
+ GitHub credential and the fallback below.
21
+ In a Legion pane, the **implementer** runs the command before every push that answers a review
22
+ (the corrective push and the final `.legion/` deletion push) and pastes its output into the
23
+ `Threads` section. The command resolves each unresolved thread whose newest submitted comment is
24
+ the opener's own `Accepted:` reply. On a thread a bot account opened that is none of Legion's
25
+ role Apps (the daemon names them, keyed by App role), the Legion reviewer's `Accepted:` also
26
+ closes it. GitHub cannot tell a CI bot, which never accepts, from a person whose `gh` is routed
27
+ to an App, so the reviewer adjudicates such a finding, and it may accept one an App-routed person
28
+ raised. The subject of a finding never closes it: the implementer's `Fixed in <commit>: …` or
29
+ `Declined: …` answers a thread and closes none. A thread either Legion App opened, a reviewer's
30
+ finding included, still needs its opener's `Accepted:`. It makes one `resolveReviewThread` per
31
+ thread, prints `resolved <url> — <whose acceptance>` (its opener's, or the Legion reviewer's on a
32
+ bot's thread, so the ledger shows which) or `left open <url> — newest reply by <login> is …`
33
+ naming why, and exits 1 naming the thread's URL and GitHub's message when GitHub refuses one.
34
+
35
+ Without a grant, page through `reviewThreads`, skip `isResolved: true`, and compare the opener
36
+ with the newest comment. Query shape, inside `repository { pullRequest { … } }`:
37
+
38
+ ```graphql
39
+ reviewThreads(first: 100, after: $after) {
40
+ pageInfo { hasNextPage endCursor }
41
+ nodes {
42
+ id isResolved
43
+ opener: comments(first: 1) { nodes { author { __typename login } } }
44
+ newest: comments(last: 1) { nodes { author { __typename login } body state } }
45
+ }
46
+ }
47
+ ```
48
+
49
+ Resolve only when the newest comment is submitted, its `author` is the opener's account (the same
50
+ `__typename` and `login`: a login alone is a string anyone may register), and its `body`, after
51
+ removing leading spaces, tabs, CR, and LF, begins `Accepted:`. Without a
52
+ grant nothing names Legion's own App logins, so this route closes a bot's thread only on its
53
+ opener's `Accepted:`: leave one the Legion reviewer accepted for the implementer's or merger's
54
+ run in a pane, or report it. For each thread to resolve:
55
+
56
+ ```graphql
57
+ mutation($threadId: ID!) {
58
+ resolveReviewThread(input: { threadId: $threadId }) { thread { isResolved } }
59
+ }
60
+ ```
61
+
62
+ Re-read `reviewThreads` and confirm that thread's `isResolved` is true. In either route, report
63
+ a refused resolution to the architect, which opens an ask for a human to resolve the thread by
64
+ hand — never skip it silently. The merger runs the command once more before publishing READY
65
+ and does not publish while any `left open` line remains.
66
+
67
+ - **The reviewer, on a re-review.** When you re-review after a corrective push, answer every
68
+ thread you opened in one of the three forms above — `Accepted:` is the only reply the
69
+ implementer's `legion threads resolve` acts on — and approve only once every thread you opened
70
+ carries your `Accepted:` reply and the implementer's run has resolved it (verify
71
+ `isResolved: true` with `gh api graphql`, never from the PR body).
package/src/server.ts CHANGED
@@ -5,6 +5,7 @@ import { agentSubject, dispatchToolSpecs, zodSchemaApi } from "@legion/contracts
5
5
  import { envoyDefaultsFromEnvironment } from "@legion/envoy-client/defaults";
6
6
  import { resolveDispatchConfig } from "@legion/envoy-client/dispatch-config";
7
7
  import { executeDispatchTool } from "@legion/envoy-client/dispatch-execute";
8
+ import { dispatchFirstSkillFile } from "@legion/envoy-client/dispatch-first";
8
9
  import { machineID } from "@legion/envoy-client/machine";
9
10
  import {
10
11
  envoyToolSpecs,
@@ -53,6 +54,20 @@ export default async (input: { serverUrl: URL }) => {
53
54
  `envoy: dispatch tools disabled — ${dispatchConfig.error ?? "no Dispatch URL configured"}`
54
55
  );
55
56
  }
57
+ // With Dispatch configured, every session carries the dispatch-first skill as an instruction
58
+ // file: OpenCode reads instruction files into the main loop's system prompt on every request
59
+ // and leaves them out of title and compaction requests. A package without the skill fails
60
+ // here, naming the file, rather than serving sessions that silently lack it.
61
+ let dispatchFirst: string | undefined;
62
+ if (dispatchConfig.enabled) {
63
+ if (skillsDirectory === undefined) {
64
+ throw new Error(`envoy: no skills directory beside ${moduleDirectory}`);
65
+ }
66
+ dispatchFirst = dispatchFirstSkillFile(skillsDirectory);
67
+ if (!existsSync(dispatchFirst)) {
68
+ throw new Error(`envoy: the dispatch-first skill ${dispatchFirst} is missing`);
69
+ }
70
+ }
56
71
  const envoyDefaults = envoyDefaultsFromEnvironment(process.env);
57
72
  const envoy = createEnvoyClient({ baseUrl: envoyDefaults.envoyUrl, fetch: globalThis.fetch });
58
73
  let activeSessionID: string | null = null;
@@ -195,7 +210,9 @@ export default async (input: { serverUrl: URL }) => {
195
210
  }
196
211
 
197
212
  return {
198
- config: (cfg: { skills?: { paths?: string[] } } & Record<string, unknown>) => {
213
+ config: (
214
+ cfg: { skills?: { paths?: string[] }; instructions?: string[] } & Record<string, unknown>
215
+ ) => {
199
216
  // Serve the bundled legion skills to every session on this serve.
200
217
  if (skillsDirectory) {
201
218
  cfg.skills ??= {};
@@ -204,6 +221,10 @@ export default async (input: { serverUrl: URL }) => {
204
221
  cfg.skills.paths.push(skillsDirectory);
205
222
  }
206
223
  }
224
+ if (dispatchFirst !== undefined) {
225
+ cfg.instructions ??= [];
226
+ if (!cfg.instructions.includes(dispatchFirst)) cfg.instructions.push(dispatchFirst);
227
+ }
207
228
  },
208
229
  event: async ({
209
230
  event,
@@ -1,98 +0,0 @@
1
- # Knowledge Injection Algorithm
2
-
3
- Canonical algorithm for injecting relevant learnings from `docs/solutions/` before phase-specific work begins. All worker workflows reference this file for the injection procedure; each workflow specifies its own keyword sources.
4
-
5
- ## Overview
6
-
7
- Before starting main work, each phase checks the learnings index for applicable prior knowledge. This surfaces patterns, pitfalls, and institutional knowledge that previous workers documented.
8
-
9
- **Injection must never block work.** If any step fails (missing index, invalid JSON, missing files, empty handoff data), skip silently and proceed with the phase's main work.
10
-
11
- ## Algorithm
12
-
13
- ### 1. Read the Index
14
-
15
- Assemble the index by reading all per-entry JSON files in `docs/solutions/.index/`:
16
-
17
- ```bash
18
- # Read and merge all entry files in .index/ directory
19
- for f in docs/solutions/.index/*.json; do
20
- [ -f "$f" ] && cat "$f"
21
- done
22
- ```
23
-
24
- Each file has the format `{ "version": 1, "entries": { "key": ["learning-path", ...] } }`. Merge all `entries` maps together, deduplicating learning paths per key.
25
-
26
- If the `.index/` directory doesn't exist or contains no valid JSON files, skip injection entirely — proceed to the phase's main work.
27
-
28
- ### 2. Extract Keywords
29
-
30
- Collect keywords from the phase-specific sources (defined in each workflow file). The extraction algorithm:
31
-
32
- 1. **Collect raw text** from the specified keyword sources (see the calling workflow's keyword source table)
33
- 2. **Tokenize**: split on whitespace, `/`, `-`, `_`, and camelCase boundaries
34
- 3. **Normalize**: lowercase all tokens
35
- 4. **Filter**: remove tokens < 3 chars and common stopwords (the, and, for, with, this, that, from, into, when, will, should, would, could, also, been, have, each, etc.)
36
- 5. **Deduplicate** tokens
37
- 6. **Extract full path segments**: e.g., `packages/daemon/src/state` — keep as-is for path matching in addition to individual tokens
38
-
39
- Also look for references to:
40
- - Source path segments (e.g., `packages/daemon/src/state/`, `serve-manager`)
41
- - Module names (e.g., "daemon", "controller", "worker", "state")
42
- - Component names (e.g., "serve-manager", "decision", "fetch")
43
- - Feature areas (e.g., "skills", "linear", "github", "review", "retro")
44
- - Integration concerns (e.g., "PR", "labels", "MCP")
45
- - Domain concepts and error keywords from the context
46
-
47
- ### 3. Match Keywords Against Index
48
-
49
- Use two matching modes against the keys in `.index`:
50
-
51
- - **Path matching**: For each key that does NOT start with `tag:`, check if any extracted keyword appears as a substring of the key (case-insensitive). Collect all matched learning file paths.
52
- - **Tag matching**: For each key that starts with `tag:`, extract the tag name (e.g., `tag:race-condition` → `race-condition`). Check if any extracted keyword matches the tag name (case-insensitive). Collect matched learning file paths.
53
-
54
- ### 4. Deduplicate and Rank
55
-
56
- - Remove duplicates (same file matched via multiple keys)
57
- - **Status filter**: For each candidate, read its YAML front matter `status` field. Exclude any file with `status: superseded`. If the file doesn't exist or has no front matter, include it (graceful degradation).
58
- - **Primary rank: tag overlap** — For each remaining candidate, read its `tags` front matter field. Count how many of its tags appear in the extracted keywords (case-insensitive). Higher overlap = higher rank.
59
- - **Secondary rank: key specificity** — Learnings matched via longer/more-specific keys rank higher (e.g., a match on `packages/daemon/src/state` outranks a match on `packages/daemon`)
60
- - **Tertiary rank: match count** — Number of distinct key matches (more matches = more relevant)
61
- - **Cap at 3 learnings maximum**
62
-
63
- ### 5. Read Matched Learnings
64
-
65
- For each matched learning file (from `docs/solutions/<path>`):
66
-
67
- 1. Read YAML front matter: extract `title` and `tags` fields
68
- 2. Skip past front matter (`---` blocks) and headings, take the first paragraph of prose (typically the Problem or Overview section)
69
- 3. Prepend structured header: `[{title} | tags: {comma-separated tags}]`
70
- 4. Truncate entire output (header + prose) to **350 characters**
71
-
72
- **If a matched file doesn't exist on disk:** Skip that entry silently (stale index entry from a file rename). Do not error.
73
-
74
- ### 6. Output Injected Learnings
75
-
76
- Output the injected learnings visibly in the session before proceeding with the phase's main work:
77
-
78
- ```
79
- ## Relevant Learnings (from docs/solutions/)
80
-
81
- 1. [docs/solutions/<path>]: [{title} | tags: {tag1}, {tag2}] <prose excerpt> (350 chars max total)
82
- 2. [docs/solutions/<path>]: [{title} | tags: {tag1}, {tag2}] <prose excerpt>
83
- 3. [docs/solutions/<path>]: [{title} | tags: {tag1}, {tag2}] <prose excerpt>
84
-
85
- (Review these for patterns and pitfalls relevant to this phase's work.)
86
- ```
87
-
88
- **If no matches found:** Output "No relevant learnings found." and proceed. Do NOT add an empty section.
89
-
90
- **Canonical identifiers:** All references to learnings use their `docs/solutions/` relative file path (e.g., `daemon/controller-lifecycle-separation.md`). These paths are the stable IDs used for injection, handoff tracking, and future aggregation. Never use titles or truncated text as identifiers.
91
-
92
- ## Fallback Behavior
93
-
94
- When a keyword source is unavailable (missing handoff data, empty fields, missing phase data), silently fall back to the next available source as defined in the calling workflow's fallback rules. Never error on missing data.
95
-
96
- ## Integration with Handoffs
97
-
98
- If the phase writes handoff data, include a `learningsInjected` field listing the `docs/solutions/` relative paths of all injected learnings. This enables downstream phases to see what knowledge was available and supports future aggregation.