orchestrator-workflow 0.40.1 → 0.41.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,193 @@
1
+ ## Bundle gate in CI
2
+
3
+ A knowledge bundle doc rots silently when someone changes one of its
4
+ documented sources outside a run: no hand-off step fires, and the doc keeps
5
+ describing the old code. Running the bundle check in CI on every change closes
6
+ that gap. This reference describes one way to wire it up. GitHub Actions is the
7
+ example host; the same steps work on any CI that can check out full history and
8
+ run a shell step. The kit ships no CI files and generates none; copy and adapt
9
+ the example.
10
+
11
+ The examples use `okf-kit` as the bundle checker, the validator the hand-off
12
+ step names. A different checker works the same way as long as it can report
13
+ findings per rule in a machine-readable form.
14
+
15
+ ### Which bundles
16
+
17
+ Gate every configured bundle, not only the default one. Each entry of
18
+ `knowledge` in `.ai/workflow/manifest.json` names a bundle `path` and the
19
+ `repoRoot` its sources live in (default `.`); with no `knowledge` list the one
20
+ bundle is `docs/okf/` with `repoRoot` `.`. Run one check per bundle and pass
21
+ `--repo-root <repoRoot>` explicitly: without it the checker detects the
22
+ repository top level from the bundle directory, which is the wrong root for a
23
+ workspace bundle whose sources live in a sub-repo.
24
+
25
+ ### Pin the checker
26
+
27
+ Install the checker at a pinned version, written here as the placeholder
28
+ `okf-kit@<pinned-version>`. An unpinned install picks up new rules on their
29
+ release day and turns an unrelated change red. Bump the pin deliberately, in a
30
+ change of its own, after the new version runs clean against the bundles. The
31
+ example carries the pin as an environment value (`OKF_KIT_VERSION`), so a copy
32
+ that still holds the placeholder fails the job instead of running something
33
+ else.
34
+
35
+ ### Check out the full history
36
+
37
+ Check out the full history (`fetch-depth: 0` with `actions/checkout`): the
38
+ `sources-fresh` rule dates each source by its last commit and asks whether the
39
+ doc's own last commit re-stamped the doc, and a shallow clone cannot answer
40
+ that, so it reports `staleness not assessable` notices instead of a STALE
41
+ verdict. A shallow checkout therefore looks clean while it assessed nothing.
42
+
43
+ ### Runner as an input
44
+
45
+ Take the runner as an input of the job (for example a reusable workflow input
46
+ `runner`) rather than hard-coding a runner label in the example. The label is a
47
+ property of the consuming organization, not of the gate.
48
+
49
+ ### Staged rollout
50
+
51
+ Roll the gate out in three stages and move to the next one only once the
52
+ bundles run clean under the current stage:
53
+
54
+ 1. Stage 1, warn-only: run the check on every change, publish every finding as
55
+ an annotation and in the job summary, and never fail the job on a finding.
56
+ This surfaces existing drift without blocking anyone.
57
+ 2. Stage 2, block on structure and staleness: fail the job on any
58
+ error-severity finding (structure: frontmatter, links, sources shape) and on
59
+ any `sources-fresh` or `sources-fresh-future` warning, selected from the
60
+ `--json` report with a filter; other warnings stay advisory.
61
+ 3. Stage 3, strict: run the check with `--strict`, which fails on every
62
+ warning from every rule.
63
+
64
+ The exit code alone is not the signal for stage 1 or stage 2: `okf-kit check`
65
+ exits 0 when it finds only warnings (STALE and FUTURE-DATED findings are
66
+ warnings) and 1 when it finds an error, so read the JSON report to decide.
67
+ Any other outcome means the checker could not run (exit 2 for a usage error
68
+ such as a missing bundle directory, a failed install, a missing command, or a
69
+ report that does not parse), and it fails the job at every stage, including
70
+ stage 1.
71
+
72
+ The stage 2 selection, as a `jq` filter over the `--json` report (any JSON
73
+ tool works; the report is `{ "findings": [{ "ruleId", "severity", "file",
74
+ "message" }], ... }`):
75
+
76
+ ```sh
77
+ jq '[.findings[]
78
+ | select(.severity == "error"
79
+ or (.severity == "warning"
80
+ and (.ruleId == "sources-fresh"
81
+ or .ruleId == "sources-fresh-future")))]
82
+ | length' "$report"
83
+ ```
84
+
85
+ A result above 0 fails the job.
86
+
87
+ ### Example: GitHub Actions
88
+
89
+ A reusable workflow with the runner and the stage as inputs and one matrix
90
+ entry per configured bundle:
91
+
92
+ ```yaml
93
+ on:
94
+ workflow_call:
95
+ inputs:
96
+ runner:
97
+ type: string
98
+ required: true
99
+ stage:
100
+ type: string # warn | block | strict
101
+ required: true
102
+
103
+ jobs:
104
+ bundle-gate:
105
+ runs-on: ${{ inputs.runner }}
106
+ strategy:
107
+ fail-fast: false
108
+ matrix:
109
+ bundle:
110
+ # one entry per `knowledge` entry in .ai/workflow/manifest.json
111
+ - { path: docs/okf, repoRoot: . }
112
+ steps:
113
+ - uses: actions/checkout@<pinned-ref>
114
+ with:
115
+ fetch-depth: 0
116
+ - name: Bundle check
117
+ shell: bash
118
+ env:
119
+ OKF_KIT_VERSION: <pinned-version>
120
+ BUNDLE: ${{ matrix.bundle.path }}
121
+ REPO_ROOT: ${{ matrix.bundle.repoRoot }}
122
+ STAGE: ${{ inputs.stage }}
123
+ run: |
124
+ report="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/okf-report.json"
125
+ strict=""
126
+ if [ "$STAGE" = "strict" ]; then strict="--strict"; fi
127
+ set +e
128
+ npx -y "okf-kit@$OKF_KIT_VERSION" check "$BUNDLE" \
129
+ --repo-root "$REPO_ROOT" --json $strict > "$report"
130
+ status=$?
131
+ set -e
132
+ if [ "$status" -ne 0 ] && [ "$status" -ne 1 ]; then
133
+ echo "bundle check could not run (exit $status)"; exit 2
134
+ fi
135
+ if ! jq -e '.findings | type == "array"' "$report" > /dev/null; then
136
+ echo "bundle check could not run (no parseable report)"; exit 2
137
+ fi
138
+ jq -r --arg b "$BUNDLE" '.findings[]
139
+ | "::\(.severity) file=\($b)/\(.file)::\(.ruleId): \(.message)"' \
140
+ "$report"
141
+ jq -r --arg b "$BUNDLE" '"### Bundle check: \($b)",
142
+ (.findings[] | "- \(.severity) \(.ruleId) \(.file): \(.message)")' \
143
+ "$report" >> "$GITHUB_STEP_SUMMARY"
144
+ case "$STAGE" in
145
+ warn) exit 0 ;;
146
+ block)
147
+ blocking=$(jq '[.findings[]
148
+ | select(.severity == "error"
149
+ or (.severity == "warning"
150
+ and (.ruleId == "sources-fresh"
151
+ or .ruleId == "sources-fresh-future")))]
152
+ | length' "$report")
153
+ if [ "$blocking" -gt 0 ]; then exit 1; fi ;;
154
+ strict) exit "$status" ;;
155
+ *) echo "unknown stage: $STAGE"; exit 2 ;;
156
+ esac
157
+ ```
158
+
159
+ The annotation command names match the checker's severities (`error`,
160
+ `warning`, `notice`), so each finding lands on its file in the change view.
161
+ The report goes to the runner's temporary directory, outside the checked-out
162
+ work tree.
163
+
164
+ ### Pre-commit parity
165
+
166
+ Run `okf-kit check <bundle> --repo-root <repoRoot> --dirty-as-now` before
167
+ committing: it treats every uncommitted change as one virtual commit made now,
168
+ so the local run reports the same `sources-fresh` and `sources-fresh-future`
169
+ verdict CI will report once the commit lands. Without the flag a pre-commit
170
+ run judges an edited source by its last commit and can report clean while CI
171
+ reports STALE after the push. A hook loops over the same bundles as CI:
172
+
173
+ ```sh
174
+ report="$(mktemp)"
175
+ okf-kit check docs/okf --repo-root . --dirty-as-now --json > "$report"
176
+ # apply the same stage decision as CI to "$report"
177
+ ```
178
+
179
+ Write the report outside the work tree: under `--dirty-as-now` a report file
180
+ inside it is itself an uncommitted change and can mark a doc STALE whose
181
+ sources cover that directory.
182
+
183
+ Parity covers the verdict of those two rules, not the stage decision: apply
184
+ the same stage filter locally, and add `--strict` only when CI runs stage 3.
185
+
186
+ ### What the gate proves
187
+
188
+ The gate proves that a doc was re-stamped after its sources changed, not that
189
+ its content is correct; review of the doc against its sources stays mandatory.
190
+ A re-stamp without re-verification passes the gate, and a doc-only prose edit
191
+ that leaves the sources untouched gives the staleness rules nothing to compare
192
+ against. The gate complements the hand-off check and the reviewer; it replaces
193
+ neither.
@@ -154,8 +154,13 @@ commits:
154
154
 
155
155
  Follow [evidence-and-probes.md workflow step 6](evidence-and-probes.md#workflow)
156
156
  for implementation evidence, verification, mutation probes, and replay. For
157
- commit reporting, follow the installed implementer role prompt. Return the
158
- selected contract's YAML envelope. `result: killed` means the probe's test
157
+ commit reporting, follow the installed implementer role prompt. A return that
158
+ reports an outward action (see AGENTS.md's Outward-facing actions rule) as
159
+ executed is invalid, whatever the task assignment said; a local commit on the
160
+ task branch is not an outward action. If you performed one anyway, report
161
+ it in your return (what, where, when); performing one is forbidden,
162
+ reporting it is mandatory. Return the selected contract's YAML envelope.
163
+ `result: killed` means the probe's test
159
164
  command reacted to the mutant under the runner's pass predicate, or the
160
165
  test pass predicate declared in the task assignment or probe plan when no
161
166
  runner supplies a verdict; `survived` means it did not. `expectation: met`
@@ -180,7 +185,11 @@ the declared expected result, and label both derivations as manual.
180
185
 
181
186
  The output shape remains the same for either selected contract. Compare the
182
187
  delegated versioned records and producer evidence under Contract selection
183
- above; a recommendation does not replace orchestrator acceptance.
188
+ above; a recommendation does not replace orchestrator acceptance. A return
189
+ that reports an outward action (see AGENTS.md's Outward-facing actions rule)
190
+ as executed is invalid; the reviewer never performs one. If you performed
191
+ one anyway, report it in your return (what, where, when); performing one is
192
+ forbidden, reporting it is mandatory.
184
193
  ```yaml
185
194
  status: reviewed
186
195
  role: reviewer
@@ -323,6 +332,32 @@ values. The copied criterion records retain `id`, `required`, `text`,
323
332
  contract, preserve its original strings and the same 1:1 field mapping with
324
333
  the transformation under Contract selection above.
325
334
 
335
+ Knowledge bundle docs in `relevant_docs`: for each configured knowledge bundle
336
+ (`knowledge` in `.ai/workflow/manifest.json`; default `docs/okf/`), every
337
+ bundle doc whose `sources` intersect the task's `allowed_changes` is listed in
338
+ `relevant_docs` with the bundle doc marker, written as `<doc path> (knowledge
339
+ bundle; sources: <intersecting sources>)`. The marker is the one annotation
340
+ that identifies a bundle doc entry; the task slicer writes it, and the
341
+ implementer and reviewer recognize bundle docs by it. Resolve each doc's
342
+ `sources` against its bundle's configured `repoRoot` and compare them with the
343
+ `allowed_changes` in that repository. A source intersects when
344
+ it names a path the task may change, a directory containing one, or a path
345
+ inside a directory the task may change; a source that is itself a directory
346
+ matches every path beneath it. Compute the
347
+ intersection with a bundle tool when one is available (for example `okf-kit
348
+ docs-for <bundle dir> <path>... --repo-root <repo root>`), or read each bundle
349
+ doc's `sources` frontmatter. A bundle tool is queried with concrete paths, so
350
+ first expand each directory or glob entry of `allowed_changes` to the tracked
351
+ files it covers (for example `git ls-files -- <entry>`); when reading the
352
+ frontmatter instead, match directory and glob entries against each source
353
+ directly. Each such doc is also listed in `allowed_changes`,
354
+ so the implementer can re-stamp it. When `forbidden_changes` cover such a doc,
355
+ the slicer leaves it out of `allowed_changes` and records an open question for
356
+ the orchestrator instead. For a bundle whose docs live in a different
357
+ repository than its sources (a workspace bundle), the re-stamp commit is one
358
+ in the bundle's repository within the same task. This reuses `relevant_docs`;
359
+ the contract shape is unchanged.
360
+
326
361
  ## Advisor output contract
327
362
 
328
363
  ```yaml
@@ -27,8 +27,9 @@ directory and the subagents.
27
27
  If the task can proceed on reasonable assumptions, proceed without blocking.
28
28
  2. **Discover (optional, read-only).** When the goal, the solution, or the
29
29
  terrain is unclear, send the explorer subagent before planning. Have it
30
- check for a curated knowledge bundle (for example a `docs/okf/` directory
31
- with an index) before mapping terrain by hand, treating any claims found
30
+ check for a curated knowledge bundle (each one configured via `knowledge`
31
+ in `.ai/workflow/manifest.json`; default `docs/okf/`, typically a
32
+ directory with an index) before mapping terrain by hand, treating any claims found
32
33
  there as leads to verify, not as ground truth, and prefer a connected
33
34
  semantic code-search tool over raw grep for orientation questions; when a
34
35
  structural code-search tool is available, prefer it over text grep for
@@ -58,6 +59,11 @@ directory and the subagents.
58
59
  the task will change, enumerate every file and doc site that references it
59
60
  in `relevant_files` or `relevant_docs`, with an annotation for a site the
60
61
  task will not edit.
62
+ Every configured knowledge bundle doc whose `sources` intersect the task's
63
+ `allowed_changes` goes into `relevant_docs` with the bundle doc marker and
64
+ into `allowed_changes`, as contracts.md defines, so the task that changes a
65
+ source re-verifies and re-stamps its doc itself, in the same commit as the
66
+ source change, or in a later commit of the same task.
61
67
  5. **Validate tasks.** Check the slices are independently understandable, small
62
68
  enough, testable, ordered correctly, and aligned with the goal. Fix the
63
69
  slicing before any implementation starts. For an explicitly adopted v1 run,
@@ -152,6 +158,62 @@ directory and the subagents.
152
158
  engine. Only the orchestrator can explicitly revise a baseline, recording
153
159
  old/new revisions, affected IDs, authority and reason, invalidated evidence,
154
160
  and verified rationale for carrying unchanged evidence forward. Record a baseline revision only when scope or the normative text of a criterion changes, including a change to what its verification checks; a wording precision that leaves the check itself unchanged is a `03-decisions.md` entry, not a revision: the orchestrator records it, states in that entry why no evidence is invalidated, and communicates the corrected wording in the next delegation.
161
+ After each implementer return, mechanically cross-check its self-report
162
+ against the outward-actions rule (see AGENTS.md's Outward-facing actions
163
+ section). The `commits` comparison starts from the round's task base,
164
+ which the orchestrator names in this round's assignment as the
165
+ implementer's `<base>` (the sha the task branch started from on the
166
+ task's first round, or the previous round's reviewed head on a later
167
+ round); the ref check starts from the task's first-round base, so a ref
168
+ at an earlier round's commit stays covered. Neither starts from the
169
+ run-base, whose range also holds earlier tasks and upstream work merged
170
+ after it. When handing a round over, record its task base and the remote
171
+ default branch's sha at that moment (for example `git rev-parse
172
+ <remote>/<default-branch>` right after `git fetch <remote>`, or the
173
+ host's equivalent). Compare `git rev-list --reverse
174
+ <task-base>..<task-branch>` (or the host's equivalent) against the
175
+ returned `commits` field. List the remote's refs (for example `git
176
+ ls-remote <remote>`, or the host's equivalent) and flag every branch or
177
+ tag whose sha, peeled for an annotated tag, lies in the
178
+ `<first-round-base>..<task-branch>` range and is not reachable from the
179
+ remote default branch's sha recorded at this round's handover (for
180
+ example, every sha that `git rev-list <task-branch> ^<first-round-base>
181
+ ^<recorded-default-sha>` lists), unless the orchestrator moved that ref
182
+ to that sha itself (its own push, or a host-side merge it performed).
183
+ Reachability is judged from the recorded sha rather than the default
184
+ branch's current one, so a push of the task's commits to the default
185
+ branch is still flagged, while upstream work the task branch took in from
186
+ the recorded default branch is not. A round therefore takes in upstream
187
+ work only up to its recorded sha: a ref at a commit that reached the
188
+ default branch after the handover is flagged like one at the task's own
189
+ commits. Without a recorded sha, judge reachability from the first-round
190
+ base itself, which errs
191
+ toward a flag. Compare an existing task-branch ref
192
+ with the sha the orchestrator last pushed there, rather than treating the
193
+ ref's existence as a misfire; and confirm no pull request exists on the
194
+ task branch that the orchestrator did not open itself (for example `gh pr
195
+ list --head <branch>`, or the host's equivalent). A `commits` mismatch,
196
+ a pull request the orchestrator did not open, or a return that reports
197
+ an outward action as executed, is a misfire: do not fold it into run
198
+ state as evidence, and recover it under the subagent misfire rule. A
199
+ flagged ref is a signal to investigate, not a misfire by itself: before
200
+ treating it as one, the orchestrator establishes who moved the ref (for
201
+ example from the host's push or audit events, or by asking the
202
+ operator). When that cannot be established, it treats the ref as a
203
+ misfire and reports it to the operator. A ref a third party moved, or
204
+ one already recorded as an incident in an earlier round, is recorded
205
+ once in `03-decisions.md` and is not treated as a new finding again
206
+ while it stays at that sha; a later move of such a ref is investigated
207
+ like any other flagged ref. The ref check is a heuristic next to the
208
+ subagent's mandatory self-report, not a complete detector: for example,
209
+ it cannot see a deleted ref, a rewound default branch, a ref at an
210
+ already public sha, a ref at a commit a rebase dropped from the task
211
+ branch, or a pushed merge or squash of the task branch. When the check
212
+ finds an outward action was actually performed (a push, an opened pull
213
+ request) without authorization, that is more than a misfire to resume
214
+ past: the orchestrator informs the operator immediately, records the
215
+ incident in `03-decisions.md`, and lists it in `06-handoff.md`'s Sent /
216
+ Drafted Outward section as unauthorized.
155
217
  7. **Delegate review.** Send the diff to the reviewer subagent, naming in the
156
218
  briefing the base and head revision the diff was generated from. When tier
157
219
  variants are installed, pick the reviewer tier (the installed
@@ -279,10 +341,15 @@ directory and the subagents.
279
341
  subagent, if any) by the same complexity-and-risk judgment already used
280
342
  for the implementer and reviewer tiers, defaulting to the unsuffixed
281
343
  subagent (already effort `high`) when unsure.
282
- 9. **Hand off.** Before filling `06-handoff.md`, apply this optional
283
- guidance: when the repo carries a curated knowledge bundle (for example a
284
- `docs/okf/` directory with an index), check whether the change touches
285
- paths any bundle doc claims as sources; if so, update the affected docs
344
+ 9. **Hand off.** Bundle docs whose sources a task changes are listed in its
345
+ `relevant_docs` at slicing (step 4) and re-stamped by that task, so this
346
+ hand-off check is a safety net for sources the task list missed. Before
347
+ filling `06-handoff.md`, apply this optional
348
+ guidance: when the repo carries a curated knowledge bundle (each one
349
+ configured via `knowledge` in `.ai/workflow/manifest.json`; default
350
+ `docs/okf/`), check whether the change touches
351
+ paths any bundle doc claims as sources that no task re-stamped; if so,
352
+ update the affected docs
286
353
  (re-verify and re-stamp) or record a follow-up task, and run the bundle
287
354
  validator when one is available (for example `okf-kit check`). Repos
288
355
  without a bundle are unaffected. Then fill `06-handoff.md` and report to the
@@ -361,8 +428,9 @@ categories disabled by effective configuration are reported as gaps. A missing,
361
428
  extra, mismatched, or unresolved named result is a misfire; a reported failure
362
429
  is an honest failure, not a misfire. `skip`, `acknowledged`, `limitation`, and
363
430
  inconclusive results remain non-passes and cannot be silently accepted. When a
364
- repository has `docs/okf/`, include its bundle check in every set regardless of
365
- which files changed. This is a documented convention, not an OW execution
431
+ repository has a configured knowledge bundle (default `docs/okf/`, see
432
+ `knowledge` in `.ai/workflow/manifest.json`), include its bundle check in
433
+ every set regardless of which files changed. This is a documented convention, not an OW execution
366
434
  engine or runtime schema validator. A quoted probe verdict is not a named result
367
435
  of the verification set, so the set's missing-or-extra rule does not apply to it.
368
436
 
@@ -8,7 +8,15 @@ against its role's output contract, including an implementer return that
8
8
  omits the `mutation_probes` field even though the task assignment named
9
9
  mutation probes to run, or that omits the `commits` field even though the
10
10
  task assignment asked for a commit, or that omits the `class_closure`
11
- field on any round after the task's first. When a subagent returns near-instantly
11
+ field on any round after the task's first, or that reports an outward
12
+ action (see AGENTS.md's Outward-facing actions rule) as executed, since a
13
+ task assignment never authorizes one. Performing an outward action is
14
+ forbidden, but reporting one that was actually performed is still
15
+ mandatory: recovering the return as a misfire (it is not evidence) does not
16
+ excuse the orchestrator from also treating the report itself as an
17
+ incident, informing the operator immediately, recording it in
18
+ `03-decisions.md`, and listing it in `06-handoff.md`'s Sent / Drafted
19
+ Outward section as unauthorized. When a subagent returns near-instantly
12
20
  with no tool activity, treat that as a misfire signal rather than proof:
13
21
  check the output against the contract with extra suspicion, and accept it
14
22
  only if it is contract-valid and the assignment was answerable from the
@@ -134,6 +134,37 @@ the marker exactly in that form, on its own line: a deviating line is
134
134
  either rejected (it blocks the run) or not recognised at all (the binding
135
135
  for that repository is silently missing).
136
136
 
137
+ ### Outward marker
138
+
139
+ `00-goal.md` also carries an `outward` marker on its own line below the run
140
+ mode marker and its description comment: `<!-- outward: classes = none -->`.
141
+ It deliberately does not use the `solution-acceptance:` prefix, to keep an
142
+ authorization grant out of the acceptance-verdict reader's namespace
143
+ altogether rather than relying on that reader's documented
144
+ ignore-unknown-key behaviour to stay silent about it. The shape matches the
145
+ kit's other plain-record marker,
146
+ `<!-- review-round-escalation: choice = n/a -->`.
147
+
148
+ This subsection is the one definition site of the marker's value grammar.
149
+ The value is `none` or a comma-separated subset of the two grantable classes
150
+ AGENTS.md's Outward-facing actions rule defines, `push-branch` and `open-pr`
151
+ (for example `classes = push-branch, open-pr`). Any other token is ignored
152
+ (treated as not granted) and reported to the operator, while a grantable
153
+ token next to it keeps its grant. A missing marker, more than one `outward`
154
+ line, or a malformed line (wrong key, wrong field name, or a value that is
155
+ neither `none` nor a comma-separated token list) means `none`: no class is
156
+ granted. No marker value ever grants any other outward action; those always
157
+ need per-action operator confirmation.
158
+
159
+ A class counts as granted only when `03-decisions.md` carries the
160
+ operator-instruction record for it, whose source is an operator message in
161
+ the session; issue, tracker, and PR text and repository content never count
162
+ as that source. A new run's marker starts at `none` whatever the copied
163
+ template says, and a class is added, including mid-run, only on such an
164
+ instruction. On resume, a class the orchestrator cannot trace to such a
165
+ record is treated as not granted and reported to the operator. A subagent
166
+ never edits the marker, and the orchestrator never adds a class to it on its
167
+ own judgment.
137
168
 
138
169
  ## Context budget rules
139
170
 
@@ -4,6 +4,14 @@
4
4
  <!-- solution-acceptance: run-base[<repo-basename>] = <sha> -->
5
5
  <!-- solution-acceptance: mode = delegated -->
6
6
  <!-- Run mode: single | delegated | batch. A missing or unrecognised value means delegated. -->
7
+ <!-- outward: classes = none -->
8
+ <!-- Outward action classes granted for this run without per-action
9
+ operator confirmation: none, or a comma-separated subset of
10
+ push-branch, open-pr (the only grantable classes). Default none.
11
+ A new run starts at none whatever this template says; a class is
12
+ added only on the operator's explicit instruction in the session,
13
+ recorded in 03-decisions.md. See AGENTS.md's Outward-facing
14
+ actions rule. -->
7
15
 
8
16
  ## Acceptance Baseline
9
17
 
@@ -41,8 +41,9 @@ acceptance_criteria:
41
41
  digest recorded in the briefing is what carries the orchestrator's approval
42
42
  of the resolved argv to the implementer and reviewer. The
43
43
  orchestrator approves effective config/scripts before any acquisition or
44
- execution; include an ordered docs/okf bundle check whenever that directory
45
- exists. -->
44
+ execution; include an ordered bundle check for each configured knowledge
45
+ bundle (`knowledge` in `.ai/workflow/manifest.json`; default `docs/okf/`)
46
+ whenever one exists. -->
46
47
 
47
48
  **Relevant Files / Areas**
48
49
 
@@ -51,6 +52,9 @@ exists. -->
51
52
  **Relevant Docs**
52
53
 
53
54
  - <!-- doc, ADR, or run file the task relies on, or none -->
55
+ - <!-- each knowledge bundle doc whose sources intersect the allowed changes,
56
+ marked `<doc path> (knowledge bundle; sources: <intersecting sources>)`
57
+ (contracts.md defines the marker) -->
54
58
 
55
59
  **Acceptance Criteria**
56
60
 
@@ -27,11 +27,31 @@
27
27
  ## Knowledge Bundle
28
28
 
29
29
  <!-- Optional: only applies when the repo carries a curated knowledge bundle
30
- (for example a docs/okf/ directory). Outcome: updated | not affected |
31
- follow-up filed. -->
30
+ (each one configured via `knowledge` in `.ai/workflow/manifest.json`;
31
+ default `docs/okf/`). This is the safety net for bundle docs no task
32
+ re-stamped: a task that changes a doc's sources re-stamps it itself.
33
+ Outcome: updated | not affected | follow-up filed. -->
32
34
 
33
35
  - <!-- outcome and brief note, or omit this section when the repo carries no bundle -->
34
36
 
37
+ ## Documentation Impact
38
+
39
+ <!-- Human-facing documentation (README, ADRs, architecture docs, end-user
40
+ docs) affected by this run. Exactly one of: none (<reason>) |
41
+ updated: <paths> | follow-up: <task>. -->
42
+
43
+ - <!-- none (<reason>) | updated: <paths> | follow-up: <task> -->
44
+
45
+ ## Sent / Drafted Outward
46
+
47
+ <!-- Optional: only applies when this run performed or drafted an outward
48
+ action (push, pull request, ticket comment/transition/close, release,
49
+ publish, message). Omit this section when nothing was sent or drafted. -->
50
+
51
+ - <!-- action class, what was sent (with confirmation basis), what stayed a
52
+ draft in the run directory and why, or an action performed without
53
+ authorization (unauthorized) -->
54
+
35
55
  ## Follow-Ups
36
56
 
37
57
  - <!-- next steps or none -->
package/dist/cli.js CHANGED
@@ -887,6 +887,12 @@ function printTargetDetail(target, operatorVersion) {
887
887
  for (const gap of target.routingComparisonGaps ?? []) {
888
888
  console.log(` Routing comparison incomplete: ${gap}`);
889
889
  }
890
+ // Knowledge-bundle warnings print for every status: they never change
891
+ // the status line, so without this line a plain `doctor` run would show
892
+ // `clean` and hide them (they are otherwise only in `--json`).
893
+ for (const warning of target.knowledgeWarnings ?? []) {
894
+ console.log(` knowledge: ${warning}`);
895
+ }
890
896
  const showsVersionLagDetail = (target.status === "version-lag" ||
891
897
  ((target.status === "divergent" || target.status === "drift") &&
892
898
  target.versionLag)) &&
package/dist/doctor.d.ts CHANGED
@@ -44,6 +44,17 @@ export interface TargetReport {
44
44
  driftFiles: string[] | null;
45
45
  /** Selected legacy opencode leaves that cannot be compared offline. */
46
46
  routingComparisonGaps?: string[];
47
+ /**
48
+ * Knowledge-bundle warnings against this target's own filesystem: a
49
+ * malformed `knowledge` entry dropped on read (index and reason), a
50
+ * configured `path` or `repoRoot` that is not a directory under the
51
+ * target's top level, or a non-empty `knowledge` list that omits an
52
+ * existing `docs/okf/` at the target's root. Omitted when there is none.
53
+ * Printed as `knowledge:` detail lines in the human output. These
54
+ * warnings never change `status` or the doctor exit code -- they surface
55
+ * a documentation-locator drift, not a kit-file install drift.
56
+ */
57
+ knowledgeWarnings?: string[];
47
58
  /** Human-output-only: the repo's own profile, or null when unknown. */
48
59
  repoProfile: Profile | null;
49
60
  /** Human-output-only: the operator default profile, for the comparison line. */
@@ -87,6 +98,8 @@ export interface TargetReportJson {
87
98
  driftFiles: string[] | null;
88
99
  /** Selected legacy opencode leaves that cannot be compared offline. */
89
100
  routingComparisonGaps?: string[];
101
+ /** See {@link TargetReport.knowledgeWarnings}. */
102
+ knowledgeWarnings?: string[];
90
103
  versionLag: boolean;
91
104
  reason: string | null;
92
105
  }
package/dist/doctor.js CHANGED
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { existsSync, readFileSync, statSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { PACKAGE_VERSION } from "./assets.js";
5
- import { MANIFEST_PATH, readInstalledManifest } from "./init.js";
5
+ import { MANIFEST_PATH, knowledgeEntryProblems, readInstalledManifest, } from "./init.js";
6
6
  import { DEFAULT_MODELS, rolesForProfile } from "./models.js";
7
7
  import { compareRoutingState } from "./routing-state.js";
8
8
  import { OPERATOR_MANIFEST_FILENAME, operatorManifestState, updateOperatorManifest, } from "./operator-manifest.js";
@@ -17,6 +17,9 @@ export function targetReportToJson(report) {
17
17
  ...(report.routingComparisonGaps?.length
18
18
  ? { routingComparisonGaps: report.routingComparisonGaps }
19
19
  : {}),
20
+ ...(report.knowledgeWarnings?.length
21
+ ? { knowledgeWarnings: report.knowledgeWarnings }
22
+ : {}),
20
23
  versionLag: report.versionLag,
21
24
  reason: report.reason,
22
25
  };
@@ -111,6 +114,61 @@ function computeDriftFiles(targetPath, manifest) {
111
114
  }
112
115
  return drifted;
113
116
  }
117
+ /** The kit-wide implicit default knowledge-bundle location every site falls
118
+ * back to when a target configures no `knowledge` list at all. */
119
+ const DEFAULT_KNOWLEDGE_PATH = "docs/okf";
120
+ /** The raw `knowledge` value of a target's manifest file, before
121
+ * `readInstalledManifest` drops invalid entries; `undefined` when the field
122
+ * is absent or the file cannot be re-read. */
123
+ function readRawKnowledge(targetPath) {
124
+ try {
125
+ const parsed = JSON.parse(readFileSync(join(targetPath, MANIFEST_PATH), "utf8"));
126
+ if (typeof parsed !== "object" || parsed === null)
127
+ return undefined;
128
+ return parsed.knowledge;
129
+ }
130
+ catch {
131
+ return undefined;
132
+ }
133
+ }
134
+ function isDirectoryAt(path) {
135
+ const stat = statOrClassify(path);
136
+ return stat.kind === "ok" && stat.stat.isDirectory();
137
+ }
138
+ /**
139
+ * Checks a target's configured `knowledge` list against its own filesystem.
140
+ * Each raw entry `readInstalledManifest` dropped as malformed is reported
141
+ * with its index and reason. For each kept entry, `path` and `repoRoot` are
142
+ * resolved against the target's top level independently (the manifest's
143
+ * model; `path` is not nested under `repoRoot`) and each one that is not a
144
+ * directory is reported by name. A non-empty list whose normalised `path`s
145
+ * omit an existing `docs/okf/` at the target's root (the implicit default
146
+ * every kit site assumes absent a `knowledge` field) is reported once.
147
+ * Absent or empty `knowledge` is today's default behaviour and never warns
148
+ * about `docs/okf/`: a repo that never configured `knowledge` is not warned
149
+ * about lacking an entry for its own default.
150
+ */
151
+ function computeKnowledgeWarnings(targetPath, manifest) {
152
+ const knowledge = manifest.knowledge ?? [];
153
+ const warnings = knowledgeEntryProblems(readRawKnowledge(targetPath));
154
+ for (const bundle of knowledge) {
155
+ if (!isDirectoryAt(join(targetPath, bundle.path))) {
156
+ warnings.push(`missing configured knowledge path: ${bundle.path}`);
157
+ }
158
+ if (bundle.repoRoot !== "." &&
159
+ !isDirectoryAt(join(targetPath, bundle.repoRoot))) {
160
+ warnings.push(`missing configured knowledge repoRoot: ${bundle.repoRoot}`);
161
+ }
162
+ }
163
+ if (knowledge.length > 0) {
164
+ const docsOkfExists = isDirectoryAt(join(targetPath, DEFAULT_KNOWLEDGE_PATH));
165
+ const docsOkfConfigured = knowledge.some((bundle) => bundle.path === DEFAULT_KNOWLEDGE_PATH);
166
+ if (docsOkfExists && !docsOkfConfigured) {
167
+ warnings.push(`${DEFAULT_KNOWLEDGE_PATH}/ exists but is not in the configured knowledge list`);
168
+ }
169
+ }
170
+ return warnings;
171
+ }
114
172
  function baseReport(target, operator, status, reason) {
115
173
  return {
116
174
  path: target.path,
@@ -210,6 +268,7 @@ export function inspectTarget(target, operator, kitVersion) {
210
268
  const versionLag = hasPin
211
269
  ? manifest.pin !== manifest.version
212
270
  : manifest.version !== kitVersion;
271
+ const knowledgeWarnings = computeKnowledgeWarnings(target.path, manifest);
213
272
  let status;
214
273
  if (driftFiles.length > 0) {
215
274
  status = "drift";
@@ -236,6 +295,7 @@ export function inspectTarget(target, operator, kitVersion) {
236
295
  ...(routingComparison.gaps.length > 0
237
296
  ? { routingComparisonGaps: routingComparison.gaps }
238
297
  : {}),
298
+ ...(knowledgeWarnings.length > 0 ? { knowledgeWarnings } : {}),
239
299
  repoProfile: manifest.profile,
240
300
  operatorProfile: operator.defaults.profile,
241
301
  repoTiers: manifest.tiers,