@antoneeo/kb-agentic-skill 1.6.0 → 1.8.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.
- package/CHANGELOG.md +75 -0
- package/README.md +9 -1
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/skills/kb-agentic-skill/ENFORCEMENT.md +46 -0
- package/skills/kb-agentic-skill/SKILL.md +106 -5
- package/skills/kb-agentic-skill/elicitation.md +4 -2
- package/skills/kb-agentic-skill/scripts/sdlc_check.py +137 -0
- package/skills/kb-agentic-skill/scripts/sdlc_core.py +104 -0
- package/skills/kb-agentic-skill/taxonomy.md +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,81 @@
|
|
|
2
2
|
|
|
3
3
|
Every significant change to this skill is recorded here.
|
|
4
4
|
|
|
5
|
+
## [1.8.0] - 2026-08-28
|
|
6
|
+
|
|
7
|
+
F-041 -- the mid-session drift field report, both items.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`check` notes an unwired or dead session-orientation hook.** `init` wires the
|
|
11
|
+
hook at init time only; pre-F-036 and agent-bootstrapped projects never got it,
|
|
12
|
+
silently. `check` now prints, right after its summary line, a one-line `[note]` when no hook is detectable
|
|
13
|
+
in `.claude/settings.json` / `.claude/settings.local.json` / `.codex/hooks.json`,
|
|
14
|
+
and a distinct note when a hook is wired but its validator does not resolve
|
|
15
|
+
(any-resolves aggregation across entries; BOM-tolerant reads; fail-open on
|
|
16
|
+
malformed config). Informational only: never the exit code, never a `validate`
|
|
17
|
+
warning -- CI stays green because the wired hook legitimately lives in the
|
|
18
|
+
git-ignored local settings file.
|
|
19
|
+
- **`remind` -- an opt-in per-turn one-line reminder (UserPromptSubmit).** orient
|
|
20
|
+
fires once; mid-session drift had no mechanical guard. One constant, zero-read
|
|
21
|
+
line re-arms the kb minimum (triage levels, the topics/INDEX.md recall trigger,
|
|
22
|
+
the capture moment) at every prompt. Never wired by `init`, never a default:
|
|
23
|
+
ENFORCEMENT par.4 documents the snippet and the ~50 tokens/prompt cost.
|
|
24
|
+
|
|
25
|
+
## [1.7.0] - 2026-08-28
|
|
26
|
+
|
|
27
|
+
F-040 — the capture moment (second-brain unit 2): the day's decisions reach the
|
|
28
|
+
ledger before they evaporate.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- **`SKILL.md` §The Capture Moment.** At a user-signed closing (phase 5 of a governed
|
|
32
|
+
unit, or any closing signal in an un-governed session), ONE sweep question carrying
|
|
33
|
+
its own inline search result: "today I recorded [...] — any other decisions from
|
|
34
|
+
today?". Scoped to TODAY, so it reaches decisions taken outside the session — facts
|
|
35
|
+
only the practitioner owns. A scheduled elicitation (the phase-3 round's family),
|
|
36
|
+
not a blocking question. The sweep is idempotent: a decision already noted at
|
|
37
|
+
phase 4 is verified, never re-captured. Channeling with explicit provenance
|
|
38
|
+
(elicited → ELICITED, synthesis → DERIVED, RULING only when resolving a contested
|
|
39
|
+
set per reconciliation §2); capture never creates topics (`unplaced.md` is never a
|
|
40
|
+
decision park); a no-topic decision is noted NOW with the placement declared as the
|
|
41
|
+
next kb unit. Declared residual: an unsignaled end captures nothing — the phase-4
|
|
42
|
+
note-when-said duty is the protection for sessions that never close.
|
|
43
|
+
- **Notes recency in the kb `orient`.** Whenever `corpus/notes/` exists: `newest
|
|
44
|
+
note: N days old` (or `no notes yet`), dated from note frontmatter `date:` with
|
|
45
|
+
mtime as fallback — named limit: mtime lies after clone/worktree, and the line
|
|
46
|
+
measures notes recency, not full ledger freshness. A missed capture is visible at
|
|
47
|
+
the next session's start.
|
|
48
|
+
- **`test_kb_capture.py`** — kb-only battery module (doctrine anchors + the five
|
|
49
|
+
recency states), outside the shared x3 manifest.
|
|
50
|
+
- Eval scenarios `capture_channels_the_days_decisions.md` and
|
|
51
|
+
`capture_stays_silent_with_nothing_to_record.md` (the negative branch: "niente"
|
|
52
|
+
writes nothing).
|
|
53
|
+
|
|
54
|
+
F-039 — the recall reflex (second-brain unit 1): the graph is read when answering,
|
|
55
|
+
not only written when distilling.
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
- **`SKILL.md` §Topic Recall — the answer-side consult.** Whenever a reply would
|
|
59
|
+
assert facts about the project's domain, the topic index is scanned and the descent
|
|
60
|
+
(`taxonomy.md` §6, new) runs in answer mode — plausibility of coverage is judged ON
|
|
61
|
+
the scanned index, never before it. Four declared verdicts (claims cited / node
|
|
62
|
+
matched no claims, surfacing `gaps:` / no coverage / index absent + regenerate),
|
|
63
|
+
emitted only when the recall ran; never faked. Once per topic per session.
|
|
64
|
+
- **The anti-echo re-touch rule.** A DERIVED claim that grounds a decision has its
|
|
65
|
+
chain walked to non-DERIVED ground (GIVEN artifact, ELICITED or RULING note) and
|
|
66
|
+
that ground cited; an unresolvable ground marks the claim unverified for
|
|
67
|
+
decision-grounding — surfaced, never silently cited. IMPORTED grounding a decision
|
|
68
|
+
is named as un-re-ratified foreign authority.
|
|
69
|
+
- **By construction: kb `orient` appends the topic router.** The overlay special-cases
|
|
70
|
+
`orient` (raw argv forwarded to the spine, never re-parsed) and appends the top of
|
|
71
|
+
`topics/INDEX.md` — or `index absent (N node files)` with the regenerate command —
|
|
72
|
+
so every session opens with the graph in sight. Fails open; spine untouched.
|
|
73
|
+
- **`test_kb_recall.py`** — new kb-only battery module (doctrine anchors + orient
|
|
74
|
+
interception), deliberately outside the shared x3 manifest.
|
|
75
|
+
- **`run_behavioral.py` fenced-block seeding** — a scenario setup entry ending in `|`
|
|
76
|
+
takes its file body from the following fenced block (common-dedented), so claims
|
|
77
|
+
tables and frontmatter'd nodes can be seeded; one-line entries unchanged.
|
|
78
|
+
- Eval scenario `recall_descends_before_answering.md`.
|
|
79
|
+
|
|
5
80
|
## [1.6.0] - 2026-08-26
|
|
6
81
|
|
|
7
82
|
F-038 — the gated rung: truthful log vocabulary and a mandated ask, no grant memory.
|
package/README.md
CHANGED
|
@@ -20,6 +20,10 @@ Two axes are kept apart on purpose — **abstraction** (a topic made of topics:
|
|
|
20
20
|
|
|
21
21
|
5. **Escalation in one batch at the end of the run**, in legal form (the claims, the reopenable sources, the dates, why the machine cannot decide). Ingestion never stops to interrogate you.
|
|
22
22
|
|
|
23
|
+
6. **Recall — the graph is read when answering, not only written when distilling.** Whenever a reply would assert facts about the project's domain, the agent scans the topic index and descends before answering from model memory — plausibility of coverage is judged ON the scanned index, never before it. Answers cite claim ids; a superseded claim is never cited without its successor; the verdict is declared only when the recall ran (`kb: <slug> → N claims cited`, a matched node with no claims surfaces its `gaps:`, and a missing index is reported as index-absent — not as a missing graph — with the regenerate command). When a DERIVED claim grounds a decision, its chain is walked to non-DERIVED ground (the given artifact or the elicited/ruled note) and that ground is cited — re-reading the agent's own synthesis is the echo chamber this rule exists to break. By construction, not exhortation: the kb `orient` opens every session with the topic router in sight.
|
|
24
|
+
|
|
25
|
+
7. **Capture — the day's decisions reach the ledger before they evaporate.** At a user-signed closing ("chiudiamo"), one sweep question — carrying its own inline result: "today I recorded X and Y — any other decisions from today?" — reaches even the decisions taken outside the session, which no transcript search could find. What surfaces lands on the existing rails (an elicited dated note, an L1 claim row) with explicit provenance; the sweep is idempotent (nothing captured twice), never creates topics, and stays silent when no one signed off. The kb `orient` shows notes recency at every session start — dated from note frontmatter, because file mtimes lie after a clone or worktree — so a missed capture is visible the next morning.
|
|
26
|
+
|
|
23
27
|
Deliberately absent: any per-node coverage or completion state. `gaps:` says what a node lacks; nothing collects it into a dashboard. A **source** does record how far it has been read, but on its own sidecar and nowhere else — the corpus index prints that fact for every artifact, including the finished ones, because a list of only what is behind is the dashboard this method refuses.
|
|
24
28
|
|
|
25
29
|
## Key features
|
|
@@ -78,7 +82,11 @@ it: a repo that vendors it gets a repo-relative command in the shared `.claude/s
|
|
|
78
82
|
(portable — commit it); otherwise the path is machine-specific and goes to the git-ignored
|
|
79
83
|
`.claude/settings.local.json`, and each teammate runs `init` once. Re-running `init` never
|
|
80
84
|
duplicates the hook, and a hook whose validator no longer resolves is reported as **broken**
|
|
81
|
-
with the correction rather than counted as installed.
|
|
85
|
+
with the correction rather than counted as installed. Since F-041, `check` also
|
|
86
|
+
prints a one-line note when a project has no orientation hook wired (or a dead
|
|
87
|
+
one), and an opt-in per-turn reminder exists for long sessions: `sdlc_check.py
|
|
88
|
+
remind`, one constant line via UserPromptSubmit -- documented in `ENFORCEMENT.md`
|
|
89
|
+
par.4, never wired by default.
|
|
82
90
|
|
|
83
91
|
## The family: three lenses, one spine
|
|
84
92
|
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kb-agentic-skill",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Knowledge-Base & Document-First protocol with risk triage, Vision governance, signal distillation and optional devPNT integration.",
|
|
5
5
|
"author": "Antonio Pinto (https://github.com/Antoneeo)"
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antoneeo/kb-agentic-skill",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Knowledge-Base & Document-First protocol for Claude Code, Gemini CLI, Google Antigravity and Codex with risk triage, Vision governance, signal distillation and optional devPNT integration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -86,6 +86,20 @@ that cannot be placed in a command safely, or a settings file it cannot write. `
|
|
|
86
86
|
prints which case applied, per client — a silent skip is what let "documented default
|
|
87
87
|
that nobody installs" survive in the first place.
|
|
88
88
|
|
|
89
|
+
**Older and agent-bootstrapped projects are silently unwired -- `check` now tells you (F-041).**
|
|
90
|
+
`init` wires the hook at init time only: a project initialized before F-036, or whose
|
|
91
|
+
docs root was bootstrapped by the agent (which runs `index`, never `init`), never
|
|
92
|
+
received it retroactively -- and until F-041 nothing said so. `check` now prints, right after its summary line, a
|
|
93
|
+
one-line `[note]` when no orientation hook is detectable (`.claude/settings.json`,
|
|
94
|
+
`.claude/settings.local.json`, `.codex/hooks.json`), and a DISTINCT note when a hook is
|
|
95
|
+
wired but the validator it names does not resolve (the wired-and-DEAD state below --
|
|
96
|
+
silence there would bless the worst of the three states). The notes are informational
|
|
97
|
+
only: never the exit code, never a `validate` warning -- CI (`validate --strict`) does
|
|
98
|
+
not see them, because the wired hook legitimately lives in the git-ignored
|
|
99
|
+
`settings.local.json`. Known residuals, accepted as honest: a client with no hook
|
|
100
|
+
mechanism (Gemini today) keeps the note -- read it as the manual-reads reminder; a
|
|
101
|
+
`.codex/hooks.json` wired in a shape other than the documented one keeps it too.
|
|
102
|
+
|
|
89
103
|
**Which settings file, and why it is not always the shared one.** The command names a
|
|
90
104
|
validator, and where that validator lives decides where the hook may be written:
|
|
91
105
|
|
|
@@ -133,6 +147,38 @@ Gemini CLI — wire the same command into its startup-hook mechanism if present;
|
|
|
133
147
|
- **Hybrid/devPNT projects**: add `--hybrid` — the hook then appends a one-line pointer to run `devpnt_mcp_get_bootstrap` for the Master Plan / Knowledge Layer and does not replicate them; the filesystem orientation (router + handoff + README) still emits.
|
|
134
148
|
- Like the CI gate (§2), if you copied `sdlc_check.py` into the repo, the hook references that copy — keep it current when you update the skill.
|
|
135
149
|
|
|
150
|
+
### Per-turn reminder: `remind` (UserPromptSubmit -- opt-in, NEVER a default)
|
|
151
|
+
|
|
152
|
+
`orient` fires once, at session start; a long session drifts after it (a compaction,
|
|
153
|
+
ten turns of other work -- the F-041 field report). `sdlc_check.py remind` prints ONE
|
|
154
|
+
constant line -- the kb minimum: the triage levels, the topic-index recall trigger, the
|
|
155
|
+
capture moment -- for the client to inject at every prompt. Cost: ~50 tokens per
|
|
156
|
+
prompt, which is exactly why `init` never wires it and never will by default: the
|
|
157
|
+
per-turn cost is the project's choice. The line is constant and zero-read, so nothing
|
|
158
|
+
repo-controlled can ride into the agent's context through it, and the vision's
|
|
159
|
+
no-per-turn-graph-queries non-goal stays honored -- the line re-arms the
|
|
160
|
+
entry-into-topic trigger; it never runs the reading itself.
|
|
161
|
+
|
|
162
|
+
Claude Code -- in the project's `.claude/settings.json` (or `settings.local.json` when
|
|
163
|
+
the validator path is machine-specific, same rule as the orientation hook above):
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"hooks": {
|
|
168
|
+
"UserPromptSubmit": [
|
|
169
|
+
{
|
|
170
|
+
"hooks": [
|
|
171
|
+
{
|
|
172
|
+
"type": "command",
|
|
173
|
+
"command": "python \"C:\\Users\\<user>\\.claude\\skills\\kb-agentic\\scripts\\sdlc_check.py\" remind"
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
136
182
|
## 5. Skill eval battery (release gate)
|
|
137
183
|
|
|
138
184
|
**Skill development only.** `test_*.py` and `evals/` are deliberately absent from the npm `files` allowlist — they never reach an installed consumer, so this section applies to whoever builds the skill, not to a project that uses it. (Consumers get `sdlc_check.py`; §1–§4 are theirs.)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kb-agentic
|
|
3
|
-
version: 1.
|
|
4
|
-
description: Knowledge-Base & Document-First protocol with risk-proportional triage, Vision as a guide, Signal Distillation, a complete Standalone mode and optional symbiosis with devPNT. Use for user documentation, knowledge extraction, SOPs, research notes, decision logs and
|
|
3
|
+
version: 1.8.0
|
|
4
|
+
description: Knowledge-Base & Document-First protocol with risk-proportional triage, Vision as a guide, Signal Distillation, a complete Standalone mode and optional symbiosis with devPNT. Use for user documentation, knowledge extraction, SOPs, research notes, decision logs, knowledge management — and for answering project questions from the claim ledger and topic graph.
|
|
5
5
|
author: Antonio Pinto (https://github.com/Antoneeo)
|
|
6
6
|
copyright: (c) 2026 Antonio Pinto
|
|
7
7
|
---
|
|
@@ -15,7 +15,7 @@ This skill guides knowledge management and documentation with a Document-First p
|
|
|
15
15
|
Support files in the skill directory:
|
|
16
16
|
- `templates.md`: templates for Vision, Knowledge ANALYSIS, Research SPIKE, SOP GUIDE, audit plan, and handoff.
|
|
17
17
|
- `portability.md`: carrying knowledge between projects — what a bundle is, why export is a closure rather than a selection, and the rule that knowledge crosses a project boundary while authority does not (`prov: IMPORTED`). Read before `export`/`import`.
|
|
18
|
-
- `taxonomy.md`: placing a claim in the topic graph — descent over the generated index, the five verdicts (EXISTS / INADEQUATE / MISSING / GENERALIZES / UNPLACED), the sibling rule, guarded re-parenting, canonicalization. Run at L3 before drafting.
|
|
18
|
+
- `taxonomy.md`: placing a claim in the topic graph — descent over the generated index, the five verdicts (EXISTS / INADEQUATE / MISSING / GENERALIZES / UNPLACED), the sibling rule, guarded re-parenting, canonicalization — and the answer-mode descent (`§6`). Run at L3 before drafting.
|
|
19
19
|
- `guides.md`: pipeline for distilling user-provided indications into `ai_docs/reference/GUIDE_[topic].md`.
|
|
20
20
|
- `vision.md`: how to write a Vision a cold reviewer can actually apply — the properties that make a rule hold, the minimum operable sections, and the blind check.
|
|
21
21
|
- `distillation.md`: from a source to claim rows — intake (content-addressed originals, stored canonical extraction, every provenance a real file), the claim table, extraction discipline, signal rules (symbiosis with `distill`).
|
|
@@ -23,7 +23,7 @@ Support files in the skill directory:
|
|
|
23
23
|
- `review.md`: the review discipline — when a review is due, how to request one, how to receive findings, how to review.
|
|
24
24
|
- `dispatch.md`: opt-in subagent execution of an approved plan.
|
|
25
25
|
- `routing.md`: which lens owns this unit of work. Read ONLY when a sibling lens skill is installed alongside this one; a single-lens install never reads it.
|
|
26
|
-
- `scripts/sdlc_check.py` + `scripts/sdlc_core.py`: the mechanical validator for the docs root (`check`, `validate`, `index`, `stale`, `mark`, `gate`, `plan`, `orient`, `migrate`, and the knowledge overlay: `graph`, `corpus`, `claim-id`, `anchor`, `export`, `import`). Two files: the core is the family's shared spine; the entry point IS the knowledge overlay — the claim ledger and topic-graph checks live inside it, so the core alone runs none of them. Copy both, or neither.
|
|
26
|
+
- `scripts/sdlc_check.py` + `scripts/sdlc_core.py`: the mechanical validator for the docs root (`check`, `validate`, `index`, `stale`, `mark`, `gate`, `plan`, `orient`, `migrate`, and the knowledge overlay: `graph`, `corpus`, `claim-id`, `anchor`, `export`, `import`, plus `remind` — the opt-in per-turn one-line reminder, ENFORCEMENT §4). Two files: the core is the family's shared spine; the entry point IS the knowledge overlay — the claim ledger and topic-graph checks live inside it, so the core alone runs none of them. Copy both, or neither.
|
|
27
27
|
- `ENFORCEMENT.md`: optional setup for CI and hooks.
|
|
28
28
|
|
|
29
29
|
Read these files only when needed. `SKILL.md` is the operating contract; the support files are progressive resources.
|
|
@@ -35,7 +35,7 @@ Read these files only when needed. `SKILL.md` is the operating contract; the sup
|
|
|
35
35
|
- **Signal & Distillation:** eliminate fluff, filler, and speculative statements; keep deterministic facts, constraints, decisions, and clear SOP steps (symbiosis with `distill`).
|
|
36
36
|
- **Lifecycle & Freshness:** mark superseded knowledge (`status: SUPERSEDED` or `DEPRECATED`) so outdated information does not cause hallucination.
|
|
37
37
|
- **Protect the Vision & User Style:** align all knowledge work with user strategic goals, operational preferences, and expected outcomes.
|
|
38
|
-
- **Map-First Navigation:** rely on `ai_docs/README.md`, `INDEX.md`, and `reference/INDEX.md` router for targeted retrieval before creating new documents.
|
|
38
|
+
- **Map-First Navigation:** rely on `ai_docs/README.md`, `INDEX.md`, and `reference/INDEX.md` router for targeted retrieval before creating new documents — and before *answering from model memory* on the project's domain (`## Topic Recall`).
|
|
39
39
|
|
|
40
40
|
## Rule Zero: Triage
|
|
41
41
|
|
|
@@ -90,6 +90,7 @@ Triage decides IF documentation is due; this table decides WHICH document each e
|
|
|
90
90
|
| a KB bundle (`export`) | Knowledge must leave this project. Export is L1 — it writes nothing into the corpus. **Importing one is L3**: a source enters the corpus and nodes are created, and a bundle is external input, so never L1 whatever its size (`portability.md`). | — |
|
|
91
91
|
| `corpus/given/*` + sidecar | A source arrives: it becomes a content-addressed artifact with a sidecar carrying digest/date/`supersedes:`. A text source is copied verbatim. A non-text source yields its stored canonical extraction — copied **beside** the original when that is small enough to keep, or **instead of it** on a large binary corpus, where the original stays where it lives and is recorded as `original_path:`/`original_sha256:` (`distillation.md` §1, which owns this rule). Never edited after ingest — the digest check on whatever `given/` holds is what enforces it. | 4 |
|
|
92
92
|
| `corpus/notes/*` | Something is said (`origin: elicited`), synthesised (`derived_from:`), or ruled (`basis:`). A note with none of the three is refused by the validator. | 4 / 5 |
|
|
93
|
+
| the day's decisions, at a user-signed closing (the Capture Moment sweep) | Verification that the phase-4 notes/claims exist; NEW `corpus/notes/*` + claim rows only for what the sweep surfaces un-captured. The event is the CLOSING, disjoint from the row above (which fires when something is SAID). | 5 / session end |
|
|
93
94
|
| `solutions/PLAN_[topic].md` + its ledger | A source too long for one context is ingested: **one task per reading window** (30 pages by default; the plan states the window used), each ending at a declared `extracted_through:`. That ledger is the register the ingestion resumes from across sessions — ingestion never builds a second one (`distillation.md` §3, `templates.md`). | 4 |
|
|
94
95
|
| `corpus/given/*.meta.md` — `extracted_through:` | Every reading window closes by advancing it (`p=<n>`, `L<n>`, `complete`). Required once any claim cites the artifact: unstated, "I am finished" cannot be falsified, which is how a sampled 200-page manual passes for an ingested one. | 4 |
|
|
95
96
|
| `INDEX.md`, `reference/INDEX.md`, `topics/INDEX.md`, `corpus/INDEX.md` | Regenerated by `sdlc_check.py index` at closure — never by hand; `validate` fails on a hand-edited one. | 5 |
|
|
@@ -142,6 +143,106 @@ Guides are consulted, created, and distilled per `guides.md`.
|
|
|
142
143
|
- **Propose proactively**: when the user hands over indications that will govern future work, propose distilling them into a guide. A proposal, never a silent write, and never from model knowledge — a guide's whole value is that every claim traces to what the user actually provided.
|
|
143
144
|
- Distill from user instructions into `ai_docs/reference/GUIDE_[topic].md`.
|
|
144
145
|
|
|
146
|
+
## Topic Recall — the answer-side consult
|
|
147
|
+
|
|
148
|
+
The graph is only a second brain if it is READ when answering, not only written when
|
|
149
|
+
distilling. The descent (`taxonomy.md`) fires here with a new trigger — the answer-side
|
|
150
|
+
twin of the guide consult above.
|
|
151
|
+
|
|
152
|
+
- **Trigger — unconditional scan on domain assertions.** Whenever the reply would assert
|
|
153
|
+
facts about the PROJECT'S domain (its topics, decisions, sources, constraints), scan
|
|
154
|
+
`topics/INDEX.md`. Plausibility of coverage is judged ON the scanned top rows, never
|
|
155
|
+
before them — "the KB probably doesn't have this" is model memory talking, the exact
|
|
156
|
+
judgement this reflex removes. Exempt: pure mechanics (running a command, editing per
|
|
157
|
+
explicit instruction), general knowledge outside the project's domain, L1 gestures.
|
|
158
|
+
- **Once per topic per SESSION.** The dedup key is the matched slug (known ex post); a
|
|
159
|
+
question plainly within an already-descended branch reuses that consult, and a
|
|
160
|
+
placement descent already run this session counts. A compaction starts a new session.
|
|
161
|
+
- **The act.** Descend per `taxonomy.md` §6 (same doctrine, answer mode). On claims,
|
|
162
|
+
answer FROM them citing claim ids; a claim whose `state` is `SUPERSEDED <id>` is never
|
|
163
|
+
cited without naming its successor.
|
|
164
|
+
- **The declared verdict — only when the recall ran**, never a per-turn ritual. Four
|
|
165
|
+
legal values:
|
|
166
|
+
- `kb: <slug> → N claims cited`
|
|
167
|
+
- `kb: <slug> → node matched, no claims` — surface the node's `gaps:` when it answers
|
|
168
|
+
the question: "the KB knows it doesn't know" is a first-class answer.
|
|
169
|
+
- `kb: no coverage` — index read, nothing fits.
|
|
170
|
+
- `kb: index absent — regenerate (sdlc_check.py index)` — the INDEX is missing, not
|
|
171
|
+
necessarily the graph (nodes routinely exist while the index lags); regenerate and
|
|
172
|
+
rescan.
|
|
173
|
+
Never fake the verdict: an always-"no coverage" verdict certifies a lookup that did
|
|
174
|
+
not happen. The `router:` and `kb:` verdicts are independent and may co-occur, one
|
|
175
|
+
line each — a how-to question can match a guide AND carry claims.
|
|
176
|
+
- **Anti-echo — the re-touch rule.** When a DERIVED claim grounds a decision the user is
|
|
177
|
+
about to take, walk its chain to non-DERIVED ground — a GIVEN artifact, an ELICITED or
|
|
178
|
+
RULING note — and cite that ground beside the claim id; re-opening only the immediate
|
|
179
|
+
target may re-read the agent's own synthesis, which is the echo chamber itself. If the
|
|
180
|
+
ground does not resolve (imported bundle without originals, external `original_path:`,
|
|
181
|
+
tombstone): say so beside the claim id and treat the claim as **unverified for
|
|
182
|
+
decision-grounding** — surfaced, never silently cited. Per-provenance stance: GIVEN's
|
|
183
|
+
source column is already the direct citation; ELICITED/RULING carry the practitioner's
|
|
184
|
+
own authority; an IMPORTED claim grounding a decision is named as un-re-ratified
|
|
185
|
+
foreign authority.
|
|
186
|
+
- **The trace persists.** When the decision is recorded (a ruling note, a diary entry),
|
|
187
|
+
that record cites the claim id AND the re-touched ground — the trace survives in the
|
|
188
|
+
corpus, not in ephemeral chat.
|
|
189
|
+
- **Under dispatch**, recall is an orchestrator concern: the orchestrator runs the
|
|
190
|
+
descent itself; a context-free subagent never runs its own. The task schema has no
|
|
191
|
+
claims field today, so until a dispatch-focused unit adds one, the orchestrator
|
|
192
|
+
inlines the cited claim rows in the task's free text — an explicit interim.
|
|
193
|
+
|
|
194
|
+
## The Capture Moment — the day's decisions reach the ledger
|
|
195
|
+
|
|
196
|
+
Capture today is what makes recall honest tomorrow: a captured elicited or ruled note
|
|
197
|
+
is exactly the non-DERIVED ground the re-touch rule above walks to. The rails all
|
|
198
|
+
exist — this section adds only the MOMENT.
|
|
199
|
+
|
|
200
|
+
- **When — user-signed closings, once per session.** Phase 5 of a governed unit, or
|
|
201
|
+
any closing signal from the user in an un-governed session ("chiudiamo", "we're
|
|
202
|
+
done"); the session-end pass that updates `handoff.md`/`project_notes.md` then runs
|
|
203
|
+
at that same closing. Never per-turn, never a standing interruption. **Declared
|
|
204
|
+
residual**: an unsignaled end (the user simply stops writing) captures nothing —
|
|
205
|
+
the phase-4 duty (a decision stated mid-session owes its note WHEN SAID) is the
|
|
206
|
+
protection for sessions that never close, and the orient's recency line makes the
|
|
207
|
+
miss visible next time. That line measures NOTES recency, not full ledger
|
|
208
|
+
freshness, and trusts frontmatter `date:` over file mtimes — mtimes lie after a
|
|
209
|
+
clone or worktree.
|
|
210
|
+
- **The sweep question.** At the closing, ONE question, carrying its own search
|
|
211
|
+
result inline: "today I recorded: [what the sweep found already on the rails] —
|
|
212
|
+
any other decisions from today to record?". Scoped to TODAY, so it reaches
|
|
213
|
+
decisions taken outside the session (a meeting, a call) that no transcript search
|
|
214
|
+
could find — facts only the practitioner owns. This is a **scheduled elicitation**
|
|
215
|
+
in the same family as the phase-3 round, not a blocking question (nothing stalls;
|
|
216
|
+
the session is ending), so the blocking form does not attach; the inline result
|
|
217
|
+
satisfies the search-first duty. "Niente" ends it.
|
|
218
|
+
- **The sweep is idempotent.** It verifies the day's decisions are on the rails; a
|
|
219
|
+
decision already noted at phase 4 is verified, never re-captured — no second note,
|
|
220
|
+
no duplicate claim row. The moment closes gaps; the phase-4 row stays the primary
|
|
221
|
+
channel.
|
|
222
|
+
- **What counts as a decision**: domain choices made or confirmed today — a value, a
|
|
223
|
+
threshold, a rule, a stated preference, a rejected alternative with its reason.
|
|
224
|
+
NOT mechanics (commands run, files edited), NOT process state (the handoff's job).
|
|
225
|
+
- **The channeling — existing rails, explicit provenance.**
|
|
226
|
+
- The user's words → `corpus/notes/*` with `origin: elicited` and a `date:`; a
|
|
227
|
+
claim row distilled from it carries `prov: ELICITED`, source = that note with a
|
|
228
|
+
legal `L<a>-<b>` locator.
|
|
229
|
+
- A synthesis of the day's discussion → a note with `derived_from:` → claim
|
|
230
|
+
`prov: DERIVED`.
|
|
231
|
+
- **RULING only when the decision resolves an existing contested set** — then
|
|
232
|
+
`reconciliation.md` §2 applies in full (mandatory `basis:`, supersession of the
|
|
233
|
+
whole prior set). A fresh conflict-free decision is captured elicited: a preference
|
|
234
|
+
is not a fact, and `basis:` is reserved for the fact the corpus lacks.
|
|
235
|
+
- A fact on an existing topic → claim row, L1 (`id` empty; `claim-id --fill`).
|
|
236
|
+
- No owning topic → the note is written NOW and the placement is declared as the
|
|
237
|
+
next kb unit. Capture never smuggles an L3 hierarchy change, and
|
|
238
|
+
`topics/unplaced.md` is never a decision park — UNPLACED is for topic-less
|
|
239
|
+
scraps and is invisible to recall.
|
|
240
|
+
- **Reconciliation is wired.** A captured claim meeting an existing claim on the
|
|
241
|
+
node classifies per `reconciliation.md` — capture can confirm, refine, coexist or
|
|
242
|
+
CONTEST, never silently overwrite.
|
|
243
|
+
- **Silence stays legal where no one signed off**: no closing signal → no question,
|
|
244
|
+
no ritual line.
|
|
245
|
+
|
|
145
246
|
## Mechanical Enforcement
|
|
146
247
|
- `scripts/sdlc_check.py index`: updates `ai_docs/INDEX.md` and `ai_docs/reference/INDEX.md`.
|
|
147
248
|
- `scripts/sdlc_check.py validate`: validates YAML frontmatter (`status`, `description`).
|
|
@@ -21,8 +21,10 @@ record is reviewable; a guess folded silently into a design is not.
|
|
|
21
21
|
## The question discipline
|
|
22
22
|
|
|
23
23
|
A question to the practitioner spends their attention and stalls the work; the
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
process *plans* that cost in exactly two places — the round below, and the
|
|
25
|
+
Capture Moment sweep at a user-signed closing (`SKILL.md`), a scheduled
|
|
26
|
+
elicitation in the same family that carries its search result inline and stalls
|
|
27
|
+
nothing. Everywhere, a question is legal only when BOTH hold:
|
|
26
28
|
|
|
27
29
|
1. **Searched first, and the search is named — with its result.** The answer is
|
|
28
30
|
not on record and not derivable from the corpus, the topic graph, `ai_docs/`,
|
|
@@ -1491,6 +1491,133 @@ def kb_cmd_import(args):
|
|
|
1491
1491
|
return 0
|
|
1492
1492
|
|
|
1493
1493
|
|
|
1494
|
+
def kb_cmd_orient(argv):
|
|
1495
|
+
"""Forward `orient` to the spine untouched, then append the topic router.
|
|
1496
|
+
|
|
1497
|
+
The recall reflex (SKILL.md section "Topic Recall") needs the graph in
|
|
1498
|
+
sight at session start -- by construction, not exhortation (F-039). The
|
|
1499
|
+
spine's orient is never re-implemented here: the raw argv goes through
|
|
1500
|
+
(the --help pattern below), and orient's flags are never mirrored into an
|
|
1501
|
+
overlay argparse -- the drift class the forward-by-default comment warns
|
|
1502
|
+
about. parse_known_args reads ONLY the two root flags the overlay itself
|
|
1503
|
+
needs to locate topics/, and ignores everything else.
|
|
1504
|
+
|
|
1505
|
+
The append fails open: a broken graph read must never break orient itself.
|
|
1506
|
+
"""
|
|
1507
|
+
try:
|
|
1508
|
+
rc = sdlc_core.main(argv)
|
|
1509
|
+
except SystemExit as e:
|
|
1510
|
+
rc = e.code if isinstance(e.code, int) else 1
|
|
1511
|
+
# Pre-bind so a throw INSIDE the router print (after root was validly
|
|
1512
|
+
# bound) does not erase it -- the recency block below must survive a
|
|
1513
|
+
# router failure with the bound root intact (closure R2 finding).
|
|
1514
|
+
root = name = None
|
|
1515
|
+
try:
|
|
1516
|
+
probe = argparse.ArgumentParser(add_help=False)
|
|
1517
|
+
probe.add_argument("--root")
|
|
1518
|
+
probe.add_argument("--docs-dir")
|
|
1519
|
+
known, _ = probe.parse_known_args(argv[1:])
|
|
1520
|
+
try:
|
|
1521
|
+
discovered, name = sdlc_core.resolve_docs_dir(known, known.root)
|
|
1522
|
+
except sdlc_core.AmbiguousDocsRoot:
|
|
1523
|
+
# Mirror the spine's degraded orient: fall back to the default
|
|
1524
|
+
# docs dir rather than losing the router on a half-migrated tree.
|
|
1525
|
+
discovered, name = None, sdlc_core.DEFAULT_DOCS_DIR
|
|
1526
|
+
root = (Path(known.root).resolve() if known.root
|
|
1527
|
+
else (discovered or sdlc_core.find_project_root()))
|
|
1528
|
+
topics = root / name / "topics"
|
|
1529
|
+
index = topics / "INDEX.md"
|
|
1530
|
+
if index.is_file():
|
|
1531
|
+
lines = sdlc_core.read_text(index).splitlines()
|
|
1532
|
+
print("\n## Topic router")
|
|
1533
|
+
cap = 30
|
|
1534
|
+
for line in lines[:cap]:
|
|
1535
|
+
print(line)
|
|
1536
|
+
if len(lines) > cap:
|
|
1537
|
+
print("(+%d more lines -- read topics/INDEX.md)" % (len(lines) - cap))
|
|
1538
|
+
elif topics.is_dir():
|
|
1539
|
+
nodes = [p for p in topics.glob("*.md") if p.name != "INDEX.md"]
|
|
1540
|
+
if nodes:
|
|
1541
|
+
print("\n## Topic router")
|
|
1542
|
+
print("index absent (%d node files) -- regenerate: sdlc_check.py index"
|
|
1543
|
+
% len(nodes))
|
|
1544
|
+
# no topics/ at all: the project has no graph; print nothing.
|
|
1545
|
+
except (Exception, SystemExit):
|
|
1546
|
+
# SystemExit included: the probe parser raises it on a dangling flag
|
|
1547
|
+
# value -- the spine already reported that usage error; the append must
|
|
1548
|
+
# never turn it into an escape. root/name keep whatever binding they
|
|
1549
|
+
# reached: pre-binding throws leave them None (recency skips), a throw
|
|
1550
|
+
# inside the router print leaves them valid (recency still runs).
|
|
1551
|
+
pass
|
|
1552
|
+
try:
|
|
1553
|
+
# Notes recency (F-040) -- its OWN try, genuinely independent of the
|
|
1554
|
+
# router: a router throw must not kill this line. It measures NOTES
|
|
1555
|
+
# recency, not full ledger freshness (an ingest feeding claims from
|
|
1556
|
+
# given/ leaves it unmoved). Date source: frontmatter `date:` first;
|
|
1557
|
+
# per-note mtime as fallback -- mtime LIES after clone/worktree (the
|
|
1558
|
+
# skill's own worktree hygiene makes that routine), which is why the
|
|
1559
|
+
# frontmatter date is primary.
|
|
1560
|
+
if root is None:
|
|
1561
|
+
return rc
|
|
1562
|
+
notes_dir = root / name / "corpus" / "notes"
|
|
1563
|
+
if notes_dir.is_dir():
|
|
1564
|
+
import datetime as _dt
|
|
1565
|
+
newest = None
|
|
1566
|
+
for p in notes_dir.glob("*.md"):
|
|
1567
|
+
stamp = None
|
|
1568
|
+
try:
|
|
1569
|
+
head = sdlc_core.read_text(p)[:600]
|
|
1570
|
+
# `date:` counts only INSIDE the frontmatter block: a
|
|
1571
|
+
# column-0 date: in a note's body must not misdate it.
|
|
1572
|
+
if head.startswith("---"):
|
|
1573
|
+
end = head.find("\n---", 3)
|
|
1574
|
+
if end != -1:
|
|
1575
|
+
m = re.search(r"^date:\s*(\d{4}-\d{2}-\d{2})",
|
|
1576
|
+
head[:end], re.M)
|
|
1577
|
+
if m:
|
|
1578
|
+
stamp = _dt.date.fromisoformat(m.group(1))
|
|
1579
|
+
except Exception:
|
|
1580
|
+
pass
|
|
1581
|
+
if stamp is None:
|
|
1582
|
+
try:
|
|
1583
|
+
stamp = _dt.date.fromtimestamp(p.stat().st_mtime)
|
|
1584
|
+
except Exception:
|
|
1585
|
+
continue
|
|
1586
|
+
if newest is None or stamp > newest:
|
|
1587
|
+
newest = stamp
|
|
1588
|
+
if newest is None:
|
|
1589
|
+
print("\nno notes yet (corpus/notes)")
|
|
1590
|
+
else:
|
|
1591
|
+
days = (_dt.date.today() - newest).days
|
|
1592
|
+
print("\nnewest note: %d days old (corpus/notes)" % max(days, 0))
|
|
1593
|
+
except (Exception, SystemExit):
|
|
1594
|
+
# Fail-open: nothing in the recency append may break orient. (The
|
|
1595
|
+
# probe parser lives in the router try above; this guard covers the
|
|
1596
|
+
# date/mtime walk and the print itself.)
|
|
1597
|
+
pass
|
|
1598
|
+
return rc
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
def kb_cmd_remind(_argv):
|
|
1602
|
+
"""F-041 item B: the per-turn one-line reminder (UserPromptSubmit, opt-in).
|
|
1603
|
+
|
|
1604
|
+
A CONSTANT, zero-read line -- the kb minimum an agent needs re-armed every
|
|
1605
|
+
prompt: triage, the recall trigger, the capture moment. Constant by
|
|
1606
|
+
contract: nothing repo-controlled may ride a line injected into the agent's
|
|
1607
|
+
context every turn, and zero reads is what keeps the per-turn cost flat.
|
|
1608
|
+
The vision's non-goal bans per-turn GRAPH queries; this line only re-arms
|
|
1609
|
+
the entry-into-topic criterion, it never runs the descent. Never wired by
|
|
1610
|
+
init and never a default: ENFORCEMENT par.4 documents the manual snippet
|
|
1611
|
+
and the ~50 tokens/prompt cost -- the project chooses. Fail-open like every
|
|
1612
|
+
hook surface: argv is ignored whole, the exit code is always 0."""
|
|
1613
|
+
print("kb-agentic: triage before acting -- L1 quick fact, L2 propagation of "
|
|
1614
|
+
"settled knowledge, L3 new knowledge unit, Spike exploration; declare "
|
|
1615
|
+
"the level. Answering about this project's domain? Read topics/INDEX.md "
|
|
1616
|
+
"under the docs root first and cite claim ids, or state no coverage. "
|
|
1617
|
+
"Decisions made today get recorded in the KB at session close.")
|
|
1618
|
+
return 0
|
|
1619
|
+
|
|
1620
|
+
|
|
1494
1621
|
def kb_cmd_help():
|
|
1495
1622
|
"""The spine's usage, then the overlay's own commands.
|
|
1496
1623
|
|
|
@@ -1512,6 +1639,8 @@ knowledge overlay (kb-agentic) -- also available:
|
|
|
1512
1639
|
anchor <path> <phrase> resolve a phrase to a verified locator span
|
|
1513
1640
|
export --out <dir> bundle a subgraph WITH the bytes its claims cite
|
|
1514
1641
|
import <dir> merge a bundle in additively (never overwrites)
|
|
1642
|
+
remind one-line per-turn reminder (UserPromptSubmit hook,
|
|
1643
|
+
opt-in -- ENFORCEMENT par.4; constant, zero-read)
|
|
1515
1644
|
|
|
1516
1645
|
index / validate / check the spine's behaviour PLUS the claim ledger and
|
|
1517
1646
|
the topic graph""")
|
|
@@ -1525,6 +1654,14 @@ def main(argv=None):
|
|
|
1525
1654
|
# 2026-08-02). Intercepted at argv[0] ONLY.
|
|
1526
1655
|
if argv and argv[0] in ("-h", "--help"):
|
|
1527
1656
|
return kb_cmd_help()
|
|
1657
|
+
# `orient` is special-cased BEFORE the overlay argparse, like --help: the
|
|
1658
|
+
# spine runs it on the raw argv, the overlay only appends the topic router.
|
|
1659
|
+
if argv and argv[0] == "orient":
|
|
1660
|
+
return kb_cmd_orient(argv)
|
|
1661
|
+
# `remind` too: it is a hook surface (UserPromptSubmit), so a stray flag
|
|
1662
|
+
# must print the line, not an argparse usage error into someone's prompt.
|
|
1663
|
+
if argv and argv[0] == "remind":
|
|
1664
|
+
return kb_cmd_remind(argv)
|
|
1528
1665
|
# Forward-by-default: anything not intercepted goes to the spine untouched.
|
|
1529
1666
|
# Never a hand-copied command tuple - that is how a spine command gets
|
|
1530
1667
|
# silently dropped (mkt_check.py ships that exact defect with `migrate`).
|
|
@@ -1646,6 +1646,109 @@ def cmd_mark(root, paths):
|
|
|
1646
1646
|
return 0
|
|
1647
1647
|
|
|
1648
1648
|
|
|
1649
|
+
# ------------------------------------------------- orientation-hook wiring
|
|
1650
|
+
# F-041: read-only mirror of the installer-side detection in lib.js
|
|
1651
|
+
# (findOrientHook + orientHookValidator). The JS owns WRITING hooks, with its
|
|
1652
|
+
# injection guards; this side only DETECTS, at `check`, so a project whose
|
|
1653
|
+
# sessions start unoriented hears about it in-project (ENFORCEMENT par.4: the
|
|
1654
|
+
# hook is wired at init only -- pre-F-036 and agent-bootstrapped projects never
|
|
1655
|
+
# got it, and a wired-but-dead hook is "the worst of the three states").
|
|
1656
|
+
# The file list is a SUPERSET of the JS one: .codex/hooks.json is documented in
|
|
1657
|
+
# ENFORCEMENT par.4 but never written by init. Entry-point names are pinned
|
|
1658
|
+
# equal on both sides.
|
|
1659
|
+
ORIENT_HOOK_ENTRY_POINTS = ("sdlc_check.py", "mkt_check.py")
|
|
1660
|
+
ORIENT_HOOK_CONFIG_FILES = (".claude/settings.json",
|
|
1661
|
+
".claude/settings.local.json",
|
|
1662
|
+
".codex/hooks.json")
|
|
1663
|
+
ORIENT_HOOK_CONFIG_CAP = 1024 * 1024 # settings are tiny; cap pathological files
|
|
1664
|
+
_ORIENT_HOOK_CMD_RE = re.compile(r" orient(\s|$)")
|
|
1665
|
+
_ORIENT_HOOK_TOKEN_RE = re.compile(r'"([^"]*)"|(\S+)')
|
|
1666
|
+
|
|
1667
|
+
|
|
1668
|
+
def _orient_hook_commands(root):
|
|
1669
|
+
"""Yield the command strings of SessionStart entries invoking an orient
|
|
1670
|
+
entry point, across every known client config under root. Fail-open: an
|
|
1671
|
+
unreadable, oversized, malformed or directory-shaped file contributes
|
|
1672
|
+
nothing. Reads are utf-8-sig -- a BOM'd settings.json (PowerShell/Notepad)
|
|
1673
|
+
must not read as unwired (declared divergence from Node's JSON.parse)."""
|
|
1674
|
+
for rel in ORIENT_HOOK_CONFIG_FILES:
|
|
1675
|
+
p = Path(root) / rel
|
|
1676
|
+
try:
|
|
1677
|
+
if not p.is_file() or p.stat().st_size > ORIENT_HOOK_CONFIG_CAP:
|
|
1678
|
+
continue
|
|
1679
|
+
data = json.loads(p.read_text(encoding="utf-8-sig"))
|
|
1680
|
+
except Exception:
|
|
1681
|
+
continue
|
|
1682
|
+
if not isinstance(data, dict):
|
|
1683
|
+
continue
|
|
1684
|
+
hooks = data.get("hooks")
|
|
1685
|
+
groups = hooks.get("SessionStart") if isinstance(hooks, dict) else None
|
|
1686
|
+
if groups is None:
|
|
1687
|
+
groups = data.get("SessionStart") # the .codex/hooks.json shape
|
|
1688
|
+
if not isinstance(groups, list):
|
|
1689
|
+
continue
|
|
1690
|
+
for group in groups:
|
|
1691
|
+
if not isinstance(group, dict):
|
|
1692
|
+
continue
|
|
1693
|
+
inner = group.get("hooks")
|
|
1694
|
+
for h in inner if isinstance(inner, list) else [group]:
|
|
1695
|
+
cmd = h.get("command") if isinstance(h, dict) else None
|
|
1696
|
+
if (isinstance(cmd, str)
|
|
1697
|
+
and any(n in cmd for n in ORIENT_HOOK_ENTRY_POINTS)
|
|
1698
|
+
and _ORIENT_HOOK_CMD_RE.search(cmd)):
|
|
1699
|
+
yield cmd
|
|
1700
|
+
|
|
1701
|
+
|
|
1702
|
+
def orient_hook_state(root):
|
|
1703
|
+
"""'wired' | 'dead' | 'unwired'.
|
|
1704
|
+
|
|
1705
|
+
A matching entry whose tokens name no validator is "cannot tell" and counts
|
|
1706
|
+
as wired (the lib.js contract: never report broken on cannot-tell). 'dead'
|
|
1707
|
+
requires every matching entry to name a validator that does not resolve:
|
|
1708
|
+
the client runs ALL SessionStart hooks, so one live entry beside a dead one
|
|
1709
|
+
still orients the session (any-resolves aggregation). Relative tokens
|
|
1710
|
+
resolve against the project root -- the vendored-validator convention."""
|
|
1711
|
+
matched = False
|
|
1712
|
+
for cmd in _orient_hook_commands(root):
|
|
1713
|
+
matched = True
|
|
1714
|
+
token = None
|
|
1715
|
+
for m in _ORIENT_HOOK_TOKEN_RE.finditer(cmd):
|
|
1716
|
+
tok = m.group(1) if m.group(1) is not None else m.group(2)
|
|
1717
|
+
if any(tok.endswith(n) for n in ORIENT_HOOK_ENTRY_POINTS):
|
|
1718
|
+
token = tok
|
|
1719
|
+
break
|
|
1720
|
+
if token is None:
|
|
1721
|
+
return "wired"
|
|
1722
|
+
tp = Path(token)
|
|
1723
|
+
if not tp.is_absolute():
|
|
1724
|
+
tp = Path(root) / tp
|
|
1725
|
+
if tp.exists():
|
|
1726
|
+
return "wired"
|
|
1727
|
+
return "dead" if matched else "unwired"
|
|
1728
|
+
|
|
1729
|
+
|
|
1730
|
+
def print_orient_hook_note(root):
|
|
1731
|
+
"""Print the FS-A wiring note (unwired / dead), or nothing when wired.
|
|
1732
|
+
|
|
1733
|
+
Informational only, by contract: never an exit code, never a validate
|
|
1734
|
+
warning (--strict CI must not fail on a hook that legitimately lives in
|
|
1735
|
+
git-ignored settings.local.json). Called by the spine's cmd_check AND by
|
|
1736
|
+
an overlay that replaces check with its own pipeline (the marketing lens
|
|
1737
|
+
does): the note is check-layer behaviour, whoever owns the check."""
|
|
1738
|
+
try:
|
|
1739
|
+
state = orient_hook_state(root)
|
|
1740
|
+
if state == "unwired":
|
|
1741
|
+
print("[note] no session-orientation hook wired (.claude/settings.json, "
|
|
1742
|
+
".claude/settings.local.json, .codex/hooks.json): sessions start "
|
|
1743
|
+
"unoriented -- run the installer's init or wire it per ENFORCEMENT.md par.4")
|
|
1744
|
+
elif state == "dead":
|
|
1745
|
+
print("[note] a session-orientation hook is wired but the validator it names "
|
|
1746
|
+
"does not resolve: sessions start unoriented -- re-run init or fix the "
|
|
1747
|
+
"command (ENFORCEMENT.md par.4)")
|
|
1748
|
+
except Exception:
|
|
1749
|
+
pass # detection must never break check
|
|
1750
|
+
|
|
1751
|
+
|
|
1649
1752
|
def cmd_check(root, strict=False, hybrid=False):
|
|
1650
1753
|
print("===== validate =====")
|
|
1651
1754
|
rc_v = cmd_validate(root, strict=strict, hybrid=hybrid)
|
|
@@ -1653,6 +1756,7 @@ def cmd_check(root, strict=False, hybrid=False):
|
|
|
1653
1756
|
rc_s = cmd_stale(root, hybrid=hybrid)
|
|
1654
1757
|
print(f"\ncheck: {'CLEAN' if not (rc_v or rc_s) else 'NOT CLEAN'} "
|
|
1655
1758
|
f"(validate rc={rc_v}, stale rc={rc_s})")
|
|
1759
|
+
print_orient_hook_note(root)
|
|
1656
1760
|
return 1 if (rc_v or rc_s) else 0
|
|
1657
1761
|
|
|
1658
1762
|
|
|
@@ -78,3 +78,12 @@ unreachable nodes as errors, but the refusal at write time is what prevents them
|
|
|
78
78
|
A merged or renamed topic keeps its file: `status: SUPERSEDED`, `redirect_to: <slug>`,
|
|
79
79
|
body emptied. Inbound references resolve through it; a claim re-placed from an old
|
|
80
80
|
source lands on the survivor instead of resurrecting the dead slug.
|
|
81
|
+
|
|
82
|
+
## 6. The same descent, answer mode
|
|
83
|
+
|
|
84
|
+
The descent above also serves ANSWERING (`SKILL.md` §Topic Recall): when a reply would
|
|
85
|
+
assert facts about the project's domain, descend before answering from model memory.
|
|
86
|
+
Differences only — no placement verdicts, no writes; the descent stops at reading the
|
|
87
|
+
matched node's claims; coverage (found / not found) replaces the five verdicts; UNPLACED
|
|
88
|
+
does not exist when reading. Everything else applies unchanged: descend don't scan,
|
|
89
|
+
follow every parent, synonyms, tombstone redirects resolve to the survivor.
|