toga-ai 1.0.565 → 1.0.566

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.
@@ -13,7 +13,7 @@
13
13
  ],
14
14
  "PreToolUse": [
15
15
  {
16
- "matcher": "Bash|Read|Grep|Glob|Edit|Write|MultiEdit|NotebookEdit|Task|WebFetch|WebSearch",
16
+ "matcher": "Bash|Read|Grep|Glob|Edit|Write|MultiEdit|NotebookEdit|Task|Agent|WebFetch|WebSearch",
17
17
  "hooks": [
18
18
  {
19
19
  "type": "command",
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-11
9
+ updated: 2026-08-12
10
10
  owners: ["dfranks", "jcardinal", "mhammontree", "ajean", "bala"]
11
11
  files:
12
12
  - _underscore/Error.php
@@ -438,20 +438,41 @@ app that has not been wired up yet.
438
438
  in `_underscore` and `library` in the **same** release, and the `/errors` console work needs
439
439
  **both** `library` and `tools` deployed before it is visible.
440
440
 
441
- ## ⚠ OPEN DEFECT (2026-08-06) — error capture is DEAD on sandbox-dev/beta: `Logs.Issue` is missing `clickupPriority`
441
+ ## ✅ RESOLVED (2026-08-12) — error capture was DEAD on sandbox-dev: `Logs.Issue` was missing SEVEN columns (originally filed as one)
442
442
 
443
- **Anyone debugging on that environment is flying blind.** The capture INSERT fails with **MySQL 1054
444
- (unknown column)** because `Logs.Issue` there has `clickupAssigneeId` and `createsClickupTask` but
445
- **not `clickupPriority`**. Consequences:
443
+ **Filed 2026-08-06 as a single missing column (`clickupPriority`); it was actually SEVEN.** On
444
+ **2026-08-12**, debugging a `/v2/surfaces/meta` 500 that returned `EO-1` with a null `error.id` and
445
+ wrote nothing to `Logs.Event`, a live `information_schema` diff of dev-sandbox `Logs.Issue` (**23
446
+ columns**) against `_Model_Core_Logs_Issue` (**30 columns**) found **seven** missing columns:
447
+ `clickupPriority` (migration `dbchanges2/Logs/2026-08-03a`) **plus the entire `2026-08-05a` lifecycle
448
+ migration** — `status`, `dtAutoResolved`, `baselineGapSeconds`, `baselineSampleCount`,
449
+ `dtBaselineAnchor`, `baselineAnchorOccurrences`. `_Model::save()` builds its INSERT from **all 30
450
+ declared fields**, so the first absent column throws **MySQL 1054 (unknown column)** and the whole
451
+ capture aborts. **Anyone debugging on that environment was flying blind.** While broken:
446
452
 
447
453
  - **No error is persisted at all** — no `Logs.Issue` row, no `Logs.Event` row, no ClickUp task.
448
- - The only trace is inline in the API response as **`identifiers.captureFailure`**. If you are
449
- looking in the database or ClickUp for an error you just triggered and finding nothing, look at the
450
- response body for `captureFailure` before concluding the error didn't happen.
451
-
452
- **Cause:** `_underscore` commit `4ebe12fe` *"Improve error fingerprinting, add ClickUp priority
453
- tracking"* shipped the code that writes the column **without its schema migration reaching that
454
- environment.** Fix is a one-column `dbchanges2` migration in the `Logs` folder.
454
+ - The only trace is inline in the API response as **`identifiers.captureFailure`** — and that is
455
+ **debug-mode-gated**, so on a deployed non-prod box with debug off the caller sees `identifiers: []`
456
+ and has *nothing*. (This blind spot is now closed — see the controller hardening below.)
457
+
458
+ **Cause:** both `dbchanges2/Logs/2026-08-03a` and `2026-08-05a` exist in the repo but **never ran on
459
+ dev-sandbox** — the runner tracks applied files per environment (see `Logs/2026-07-29a`), and these two
460
+ were skipped there. The originally-filed "one missing column, needs a one-column migration" was correct
461
+ about the mechanism but **undercounted the drift** — reading the 2026-08-06 note alone would have left
462
+ six columns still missing.
463
+
464
+ **Resolution (2026-08-12):** **re-applied the two existing migrations (`2026-08-03a`, then `2026-08-05a`)
465
+ to dev-sandbox** — no new migration was written; capture came back to life immediately (`Logs.Issue`/
466
+ `Logs.Event` rows written, `error.id` populated). **Still verify the same drift on beta and every other
467
+ non-prod env** before trusting their capture.
468
+
469
+ **Controller hardening (api2, 2026-08-12):** `api2/Controller/Index.php` now surfaces
470
+ `identifiers.captureFailure` on **every environment except production** (`_Environment::$name !==
471
+ 'production'`), even with debug mode off — in both the `execute()` catch and the DB-bootstrap catch. The
472
+ exception `message`/`trace` stay debug-only (they carry `_Database::register()` frame-arg credentials);
473
+ `captureFailure` names only *why the recorder failed* and cannot leak them. A dead capture layer now
474
+ announces itself instead of returning empty `identifiers`, so this class of `Logs` schema drift can never
475
+ again be silent on a non-prod box.
455
476
 
456
477
  **The general lesson (this is the pipeline's structural weak point):** error capture is the one
457
478
  subsystem whose own failure cannot be reported through itself. A schema drift here does not degrade
@@ -631,6 +652,17 @@ clientUserId). **Neither was built.** As built instead:
631
652
 
632
653
  ## Change history
633
654
 
655
+ - 2026-08-12 — **Corrected the 2026-08-06 open defect and marked it RESOLVED.** The sandbox-dev
656
+ `Logs.Issue` drift was **seven** missing columns, not one: `clickupPriority` (`2026-08-03a`) **plus the
657
+ whole `2026-08-05a` lifecycle migration** (`status`, `dtAutoResolved`, `baselineGapSeconds`,
658
+ `baselineSampleCount`, `dtBaselineAnchor`, `baselineAnchorOccurrences`) — found by an
659
+ `information_schema` diff (live 23 cols vs. model 30 cols) while debugging a `/v2/surfaces/meta` 500
660
+ that returned `EO-1` with a null `error.id` and wrote no `Logs.Event`. Both migrations existed but had
661
+ **never run on dev-sandbox** (the runner tracks applied files per environment); **re-applying the two
662
+ existing files** restored capture — no new migration. Also **hardened `api2/Controller/Index.php`** to
663
+ surface `identifiers.captureFailure` on every non-production environment even with debug off
664
+ (`message`/`trace` stay debug-only, as they carry DB creds), so a dead capture layer can no longer be
665
+ invisible on a non-prod box. (jcardinal)
634
666
  - 2026-08-11 — Sharpened the `error.id` lookup recipe from a production incident investigation
635
667
  (Compass, api2 500s): **`status = 'RESOLVED'` with `dtAutoResolved` set and `isManaged = 0` is a
636
668
  cron auto-close after a quiet window, not a fix** — it is indistinguishable from a real fix on
@@ -0,0 +1,87 @@
1
+ ---
2
+ type: session
3
+ slug: api2-error-capture-schema-drift
4
+ title: Fix invisible api2 500s (sandbox-dev Logs.Issue schema drift killed error capture); harden the recorder and the kickoff KB-priming gate
5
+ author: jcardinal
6
+ repos: [api2, _underscore, dbchanges2]
7
+ framework: "2.0"
8
+ client: shared
9
+ status: active
10
+ created: 2026-08-12
11
+ updated: 2026-08-12
12
+ ---
13
+
14
+ # Session: api2-error-capture-schema-drift
15
+ **Date:** 2026-08-12
16
+ **Project/Repo:** api2, _underscore, dbchanges2 (2.0) + harness (`~/toga-tech` skills/hooks)
17
+ **Task:** A `/v2/surfaces/meta` 500 on dev.sandbox returned `EO-1` with a null `error.id` and wrote
18
+ nothing to `Logs.Event` — undebuggable. Find why nothing is recorded and make errors visible.
19
+
20
+ ---
21
+
22
+ ## What was wrong (root cause)
23
+
24
+ api2's `Controller/Index.php` catches `Throwable` from `V2.php::execute()` and calls
25
+ `_Error::captureException()` to record the error and mint an `error.id`. `captureException()` does
26
+ `$issue->save()`, and `_Model::save()` builds its INSERT from **all 30 fields declared on
27
+ `_Model_Core_Logs_Issue`**. Live **dev-sandbox `Logs.Issue` had only 23 columns** — the first missing
28
+ one throws **MySQL 1054** and the whole capture aborts (returns `null`): no `Logs.Issue` row, no
29
+ `Logs.Event` row, null `error.id`.
30
+
31
+ Seven columns were missing (an `information_schema` diff, not the migration files — the files lag):
32
+ - `clickupPriority` — `dbchanges2/Logs/2026-08-03a`
33
+ - `status`, `dtAutoResolved`, `baselineGapSeconds`, `baselineSampleCount`, `dtBaselineAnchor`,
34
+ `baselineAnchorOccurrences` — the whole `dbchanges2/Logs/2026-08-05a` lifecycle migration
35
+
36
+ Both migrations existed but had **never run on dev-sandbox** (the runner tracks applied files per
37
+ environment — see `Logs/2026-07-29a`). The KB had this filed (2026-08-06) as **one** missing column;
38
+ it was actually seven.
39
+
40
+ **Second, compounding blind spot:** capture's only failure signal is
41
+ `identifiers.captureFailure`, which the controller emitted **only in debug mode**. The deployed
42
+ sandbox box runs with debug off, so the caller got `identifiers: []` — no hint at all.
43
+
44
+ ## What was done
45
+
46
+ 1. **Schema (fix that unblocked it):** re-applied the two existing migrations (`2026-08-03a` then
47
+ `2026-08-05a`) to dev-sandbox. No new migration. Capture came back immediately — confirmed fixed by
48
+ the developer. **Still verify beta and other non-prod envs for the same drift.**
49
+ 2. **Observability hardening (`api2/Controller/Index.php`):** surface `identifiers.captureFailure` on
50
+ **every environment except production** (`_Environment::$name !== 'production'`, new constant
51
+ `PRODUCTION_ENVIRONMENT_NAME`), even with debug off — in both the `execute()` catch and the
52
+ DB-bootstrap catch. `message`/`trace` stay **debug-only** (they carry `_Database::register()`
53
+ frame-arg DB credentials); `captureFailure` names only *why the recorder failed*, so it cannot leak
54
+ them. A dead capture layer now announces itself instead of returning empty `identifiers`.
55
+ *(Working tree only — not committed; a project repo, so the developer deploys it.)*
56
+ 3. **KB doc corrected:** `_underscore/features/error-reporting-issue-event.md` — the 2026-08-06 open
57
+ defect updated from one column to seven and marked RESOLVED, with the controller-hardening note.
58
+
59
+ ## Harness change (kickoff KB-priming enforcement)
60
+
61
+ Prompted by a process failure this session: after `/kickoff`'s interview + preflight, the KB
62
+ `context-primer` was spawned **in the background** while repo code was read **in parallel** — the team
63
+ knowledge base was treated as a side-channel instead of the first source. Fixed so it cannot recur:
64
+
65
+ - **`scripts/hooks/kickoff-gate.js`** is now **two-stage**: preflight no longer releases the gate — it
66
+ advances `armed → primer`. In `primer`, all repo `Read/Grep/Glob/Edit/Write` and every non-primer
67
+ agent stay **blocked** until the **`context-primer` subagent is spawned in the foreground**
68
+ (`run_in_background: false`); that spawn releases the gate. Reading `repo-path-<repo>` memories and
69
+ `AskUserQuestion` stay allowed.
70
+ - **`.claude/settings.json`** — added `Agent` to the gate's `PreToolUse` matcher so it fires on
71
+ `Agent`-tool spawns.
72
+ - **`skills/kickoff/SKILL.md`** — Step 4 rewritten to mandate KB-first, foreground, wait-for-it
73
+ subagent priming *before any other research* (even files named in the `/kickoff` sentence); the
74
+ top gate prose now describes the two stages.
75
+
76
+ ## Gotchas worth remembering
77
+ - `_Model::save()` INSERTs **every declared field** — one missing `Logs` column silently kills ALL
78
+ capture on that environment (the recorder can't report its own failure). Any column added to a
79
+ `Logs` table must land its migration in **every** environment.
80
+ - Read the **live** `Logs` schema (`information_schema` / `toga_describe_table`), not the `dbchanges2`
81
+ migration files — the files lag the deployed schema.
82
+ - To debug an api2 500 you can't see: the `error.id` (`<Issue.reference>-<Event.eventNumber>`) →
83
+ `Logs.Issue.trace` by the reference (part before the dash) is the real stack; the response omits it.
84
+
85
+ ## Related
86
+ - [[error-reporting-issue-event]] (the capture pipeline this session repaired + documented)
87
+ - [[surface-meta-option]] (the endpoint whose 500 surfaced the blind spot)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.565",
3
+ "version": "1.0.566",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",
@@ -2,32 +2,44 @@
2
2
  'use strict';
3
3
 
4
4
  /*
5
- * kickoff-gate.js — DETERMINISTIC enforcement of the /kickoff priming gate.
5
+ * kickoff-gate.js — DETERMINISTIC, TWO-STAGE enforcement of the /kickoff priming gate.
6
6
  *
7
- * Why this exists: the kickoff SKILL.md states a "hard gate" — when /kickoff is
8
- * invoked you MUST complete priming (Steps 0–6) BEFORE touching the task with any
9
- * Read/Grep/Glob/Edit/Write/Task call. Prose alone did not stop the model from
10
- * jumping straight to investigation when /kickoff was followed by a long, specific
11
- * task description. This hook makes the gate mechanical, not advisory.
7
+ * Why this exists: the kickoff SKILL.md states a "hard gate" — when /kickoff is invoked
8
+ * you MUST prime BEFORE touching the task. Prose alone did not stop the model from
9
+ * jumping to investigation when /kickoff was followed by a detailed task description.
10
+ * A first version of this hook released the gate the instant `kickoff-preflight` ran —
11
+ * but preflight only resolves the load-set; it does NOT read the team knowledge base.
12
+ * The model could (and did) run preflight, then read repo code BEFORE the team KB was
13
+ * primed, treating the knowledge base as a side-channel instead of the first source.
12
14
  *
13
- * It registers for TWO events from a single file:
15
+ * The team knowledge base is the whole point of kickoff — it is the distilled, shared
16
+ * gold the team maintains, and it must be primed FIRST, via the `context-primer`
17
+ * subagent, before ANY other research. This hook now enforces that in TWO stages:
14
18
  *
15
- * UserPromptSubmit → if the submitted prompt invokes /kickoff, ARM the gate:
16
- * write a session-scoped lock and inject a forceful reminder.
19
+ * Stage 1 ARMED (set by /kickoff) — allow ONLY the read-only priming steps
20
+ * (version check, team-repo probe, manifest, interview) and
21
+ * `kickoff-preflight`. Everything else is blocked. Running
22
+ * `kickoff-preflight` ADVANCES the gate to stage 2 — it no longer
23
+ * releases it.
17
24
  *
18
- * PreToolUse → while the gate is armed for THIS session, ALLOW only the
19
- * read-only priming steps (the priming Bash commands + the
20
- * interview via AskUserQuestion/Skill, which don't match the
21
- * PreToolUse matcher) and BLOCK every task tool
22
- * (Read/Grep/Glob/Edit/Write/MultiEdit/NotebookEdit/Task/
23
- * WebFetch/WebSearch and any non-priming Bash). The lock
24
- * AUTO-CLEARS the moment `knowledge.js kickoff-preflight`
25
- * runs — the definitive priming step that precedes Step 4's
26
- * first Read. So the model literally cannot investigate or
27
- * edit the task until it has actually primed.
25
+ * Stage 2 PRIMER (set by preflight) — the load-set is known but the team KB is not
26
+ * yet primed. Block all repo research (Read/Grep/Glob of code, Edit,
27
+ * Write, and every non-primer agent) until the `context-primer`
28
+ * subagent is spawned IN THE FOREGROUND (run_in_background: false),
29
+ * so the model actually waits for the briefing. Spawning it releases
30
+ * the gate. Reading repo-path MEMORY files and asking the developer
31
+ * remain allowed (they are part of priming).
28
32
  *
29
- * Fail-open: any internal/parse error allows the tool (a crashing gate must never
30
- * brick every session). The SKILL.md prose remains the backup.
33
+ * Net effect: after the interview, the model literally cannot read a single repo file,
34
+ * grep the code, or spawn any other agent until it has primed from the team KB via the
35
+ * context-primer subagent. KB-first, via subagent, without exception.
36
+ *
37
+ * Registered for UserPromptSubmit (arm) + PreToolUse (enforce). A defensive
38
+ * PostToolUse branch is a no-op — release is spawn-time in PreToolUse, so the gate can
39
+ * never get stuck waiting on an event that did not fire.
40
+ *
41
+ * Fail-open: any internal/parse error allows the tool (a crashing gate must never brick
42
+ * every session). The SKILL.md prose remains the backup.
31
43
  *
32
44
  * Escape hatch: set KICKOFF_GATE_DISABLED=1 to disable enforcement entirely.
33
45
  */
@@ -61,10 +73,11 @@ function readLock() {
61
73
  try { return JSON.parse(fs.readFileSync(lockPath(), 'utf8')); } catch (e) { return null; }
62
74
  }
63
75
 
64
- function writeLock(sessionId) {
76
+ // phase: 'armed' (priming not started) → 'primer' (preflight ran, team KB not yet primed).
77
+ function writeLock(sessionId, phase) {
65
78
  try {
66
79
  fs.mkdirSync(path.dirname(lockPath()), { recursive: true });
67
- fs.writeFileSync(lockPath(), JSON.stringify({ session: sessionId || null, ts: Date.now() }) + '\n');
80
+ fs.writeFileSync(lockPath(), JSON.stringify({ session: sessionId || null, ts: Date.now(), phase: phase || 'armed' }) + '\n');
68
81
  } catch (e) { /* non-fatal */ }
69
82
  }
70
83
 
@@ -84,7 +97,7 @@ function invokesKickoff(prompt) {
84
97
  }
85
98
 
86
99
  /* Bash commands that ARE the read-only priming steps (Steps 0–3). Everything else
87
- * is task work and stays blocked until preflight clears the gate. */
100
+ * is task work and stays blocked. */
88
101
  function isPrimingBash(command) {
89
102
  if (typeof command !== 'string') return false;
90
103
  return /knowledge\.js|kickoff-preflight|toga-ai\.version|npm\s+view\s+toga-ai|npx\s+toga-ai|registry\.json|rev-parse/i.test(command);
@@ -94,58 +107,107 @@ function isPreflightBash(command) {
94
107
  return typeof command === 'string' && /kickoff-preflight/i.test(command);
95
108
  }
96
109
 
97
- /* Tools that constitute touching the task — blocked while the gate is armed. */
110
+ /* A Read of a repo-path (or any) MEMORY file is part of priming (Step 3 resolves each
111
+ * repo's local path from a repo-path-<repo> memory). Allowed even in the primer stage;
112
+ * everything else under Read is repo research and stays blocked until the KB is primed. */
113
+ function isMemoryReadPath(p) {
114
+ if (typeof p !== 'string' || !p) return false;
115
+ const norm = p.replace(/\\/g, '/');
116
+ return /\.claude\/projects\/.+\/memory\//i.test(norm);
117
+ }
118
+
119
+ /* The subagent that primes the team knowledge base. It — and only it — may run in the
120
+ * primer stage. */
121
+ const PRIMER_SUBAGENT = 'context-primer';
122
+
123
+ /* Tools that constitute touching the task — blocked while the gate is armed. Both the
124
+ * classic `Task` tool and this harness's `Agent` tool are included. */
98
125
  const BLOCKED_TOOLS = new Set([
99
126
  'Read', 'Grep', 'Glob', 'Edit', 'Write', 'MultiEdit',
100
127
  'NotebookEdit', 'Task', 'Agent', 'WebFetch', 'WebSearch',
101
128
  ]);
102
129
 
103
- function blockMessage() {
130
+ function armedMessage() {
104
131
  return [
105
- '🛑 KICKOFF GATE — priming is not complete.',
132
+ '🛑 KICKOFF GATE (stage 1 of 2) — priming has not started.',
106
133
  '',
107
- '/kickoff was invoked this session. You MUST finish priming BEFORE touching the task',
108
- 'with any Read/Grep/Glob/Edit/Write/Task call — no matter how detailed the request after',
109
- '/kickoff looks. That trailing text is the Step 2 task description, NOT permission to start.',
134
+ '/kickoff was invoked this session. Finish the read-only priming steps BEFORE any',
135
+ 'Read/Grep/Glob/Edit/Write/Task — no matter how detailed the request after /kickoff',
136
+ 'looks. That trailing text is the Step 2 task description, NOT permission to start.',
110
137
  '',
111
- 'Do this now, in order (these are allowed while the gate is armed):',
138
+ 'Allowed now, in order:',
112
139
  ' • Step 0 version check (cat .claude/toga-ai.version ; npm view toga-ai version)',
113
140
  ' • Step 1 resolve the team repo (probe for knowledge/registry.json)',
114
141
  ' • Step 2 interview the developer (framework / layer / repo(s) / client / task)',
115
- ' • Step 3 node "<TEAM_REPO>/knowledge.js" kickoff-preflight ... ← this AUTO-CLEARS the gate',
116
- ' • Step 4+ THEN read the knowledge docs and start the task',
142
+ ' • Step 3 node "<TEAM_REPO>/knowledge.js" kickoff-preflight ...',
117
143
  '',
118
- 'The gate releases automatically the instant kickoff-preflight runs.',
144
+ 'Running kickoff-preflight ADVANCES the gate to stage 2 (team-KB priming). It no',
145
+ 'longer releases the gate — priming the knowledge base does.',
119
146
  '(Emergency override only: set KICKOFF_GATE_DISABLED=1.)',
120
147
  ].join('\n');
121
148
  }
122
149
 
150
+ function primerMessage() {
151
+ return [
152
+ '🛑 KICKOFF GATE (stage 2 of 2) — the TEAM KNOWLEDGE BASE has not been primed yet.',
153
+ '',
154
+ 'kickoff-preflight resolved the load-set, but the team KB is the FIRST source you must',
155
+ 'prime from — before ANY other research. Do NOT read repo code, grep/glob the source,',
156
+ 'edit, or spawn any other agent yet — not even files named in the /kickoff sentence.',
157
+ '',
158
+ 'Do this now (Step 4): spawn the context-primer subagent to read the resolved knowledge',
159
+ 'docs and return the briefing —',
160
+ ' Agent tool · subagent_type: "context-primer" · run_in_background: false',
161
+ ' pass it: TEAM_REPO, the full kickoff-preflight JSON, and the task description.',
162
+ 'Wait for its briefing. THEN read repo code and start the work.',
163
+ '',
164
+ 'Allowed meanwhile: reading repo-path-<repo> memory files, AskUserQuestion (to ask for',
165
+ 'a repo path), and knowledge.js priming commands.',
166
+ '(Emergency override only: set KICKOFF_GATE_DISABLED=1.)',
167
+ ].join('\n');
168
+ }
169
+
170
+ function foregroundMessage() {
171
+ return [
172
+ '🛑 KICKOFF GATE (stage 2 of 2) — spawn the context-primer in the FOREGROUND.',
173
+ '',
174
+ 'Pass run_in_background: false so team-KB priming actually COMPLETES before you',
175
+ 'continue. A backgrounded primer lets you race ahead and read repo code before the',
176
+ 'knowledge-base briefing lands — the exact failure this gate exists to prevent.',
177
+ ].join('\n');
178
+ }
179
+
123
180
  function main() {
124
181
  if (process.env.KICKOFF_GATE_DISABLED === '1') process.exit(0);
125
182
 
126
183
  const data = readPayload();
127
184
 
128
185
  // ---- UserPromptSubmit: arm the gate when /kickoff is invoked ----
129
- // Identify by the presence of a prompt and absence of a tool call.
130
186
  const isPromptEvent =
131
187
  data.hook_event_name === 'UserPromptSubmit' ||
132
188
  (typeof data.prompt === 'string' && !data.tool_name);
133
189
 
134
190
  if (isPromptEvent) {
135
191
  if (invokesKickoff(data.prompt)) {
136
- writeLock(data.session_id);
192
+ writeLock(data.session_id, 'armed');
137
193
  // stdout from UserPromptSubmit is injected into the model's context.
138
194
  console.log(
139
- '<system-reminder>KICKOFF GATE ARMED. Before ANY other tool call, complete priming ' +
140
- 'Steps 0–6 of the kickoff skill — version check, resolve team repo, interview, then ' +
141
- 'run `knowledge.js kickoff-preflight` (which releases the gate), THEN read docs and work. ' +
142
- 'The trailing task description is Step 2 input, not permission to start. ' +
143
- 'A hook will block Read/Grep/Edit/Write/Task until preflight runs.</system-reminder>'
195
+ '<system-reminder>KICKOFF GATE ARMED (two stages). Stage 1: complete read-only ' +
196
+ 'priming (version check, resolve team repo, interview) then run `knowledge.js ' +
197
+ 'kickoff-preflight`. Stage 2: preflight does NOT release the gate — you must FIRST ' +
198
+ 'prime the team knowledge base via the `context-primer` subagent (foreground, ' +
199
+ 'run_in_background: false) BEFORE any repo Read/Grep/Glob/Edit/Write or other agent. ' +
200
+ 'The trailing task description is Step 2 input, not permission to start.</system-reminder>'
144
201
  );
145
202
  }
146
203
  process.exit(0);
147
204
  }
148
205
 
206
+ // ---- PostToolUse: no-op. Release is spawn-time in PreToolUse, so the gate can never
207
+ // hang waiting on a completion event. Present defensively so a PostToolUse wiring
208
+ // never falls through to the PreToolUse enforcement below. ----
209
+ if (data.hook_event_name === 'PostToolUse') process.exit(0);
210
+
149
211
  // ---- PreToolUse: enforce while armed for THIS session ----
150
212
  const lock = readLock();
151
213
  if (!lock) process.exit(0);
@@ -158,22 +220,54 @@ function main() {
158
220
  if (data.session_id && lock.session && !sameSession) process.exit(0);
159
221
 
160
222
  const tool = data.tool_name || '';
161
- const command = (data.tool_input && data.tool_input.command) || '';
223
+ const toolInput = data.tool_input || {};
224
+ const command = toolInput.command || '';
225
+ const phase = lock.phase || 'armed';
162
226
 
163
- if (tool === 'Bash') {
164
- if (isPreflightBash(command)) { clearLock(); process.exit(0); } // priming reached — release
165
- if (isPrimingBash(command)) process.exit(0); // other read-only priming step
166
- console.log(blockMessage());
167
- process.exit(2);
227
+ // ================= STAGE 1: ARMED — only priming Steps 0–3 =================
228
+ if (phase === 'armed') {
229
+ if (tool === 'Bash') {
230
+ // preflight ADVANCES to stage 2 (does NOT release) — the team KB is still unprimed.
231
+ if (isPreflightBash(command)) { writeLock(lock.session, 'primer'); process.exit(0); }
232
+ if (isPrimingBash(command)) process.exit(0); // other read-only priming step
233
+ console.log(armedMessage());
234
+ process.exit(2);
235
+ }
236
+ if (BLOCKED_TOOLS.has(tool)) { console.log(armedMessage()); process.exit(2); }
237
+ process.exit(0); // AskUserQuestion, Skill, TodoWrite, … — part of the interview
168
238
  }
169
239
 
170
- if (BLOCKED_TOOLS.has(tool)) {
171
- console.log(blockMessage());
172
- process.exit(2);
240
+ // ============ STAGE 2: PRIMER — team KB must be primed via subagent first ============
241
+ if (phase === 'primer') {
242
+ if (tool === 'Agent' || tool === 'Task') {
243
+ if (toolInput.subagent_type === PRIMER_SUBAGENT) {
244
+ // Must be foreground so the model waits for the briefing before doing anything else.
245
+ // (The classic `Task` tool runs synchronously; only the async `Agent` tool needs the
246
+ // explicit run_in_background: false.)
247
+ if (tool === 'Agent' && toolInput.run_in_background !== false) {
248
+ console.log(foregroundMessage());
249
+ process.exit(2);
250
+ }
251
+ clearLock(); // team-KB priming is underway in the foreground — release the gate
252
+ process.exit(0);
253
+ }
254
+ console.log(primerMessage()); // any other agent must wait until the KB is primed
255
+ process.exit(2);
256
+ }
257
+ if (tool === 'Read') {
258
+ if (isMemoryReadPath(toolInput.file_path || toolInput.path)) process.exit(0); // repo-path lookup
259
+ console.log(primerMessage());
260
+ process.exit(2);
261
+ }
262
+ if (tool === 'Bash') {
263
+ if (isPrimingBash(command)) process.exit(0);
264
+ console.log(primerMessage());
265
+ process.exit(2);
266
+ }
267
+ if (BLOCKED_TOOLS.has(tool)) { console.log(primerMessage()); process.exit(2); } // Grep/Glob/Edit/Write/…
268
+ process.exit(0); // AskUserQuestion (ask for a repo path), Skill, … — allowed
173
269
  }
174
270
 
175
- // Anything else (AskUserQuestion, Skill, TodoWrite, ExitPlanMode, …) is part of
176
- // priming/interview — allow.
177
271
  process.exit(0);
178
272
  }
179
273
 
@@ -23,15 +23,24 @@ description: Start-of-session context loader for TOGA Technology projects. Run t
23
23
  > Only after Step 5's "primed and ready" summary (and Step 6's plan, for non-trivial work)
24
24
  > may you touch the task itself.
25
25
  >
26
- > **This gate is mechanically enforced — it is not just prose.** A hook
26
+ > **This gate is mechanically enforced in TWO stages — it is not just prose.** A hook
27
27
  > (`hooks/toga/kickoff-gate.js`, wired on `UserPromptSubmit` + `PreToolUse`) arms the
28
28
  > instant `/kickoff` is invoked and will **hard-block** `Read`, `Grep`, `Glob`, `Edit`,
29
- > `Write`, `Task`, and any non-priming `Bash` until you actually prime. The only calls it
30
- > permits while armed are the read-only priming steps (the version check, team-repo probe,
31
- > `knowledge.js manifest`, the developer interview) and `knowledge.js kickoff-preflight` —
32
- > **running preflight (Step 3) is what releases the gate.** So the correct response to a
33
- > block message is never to fight it: run Steps 0–3, and the moment preflight executes you
34
- > are free to read docs and work. (Emergency override only: `KICKOFF_GATE_DISABLED=1`.)
29
+ > `Write`, `Task`/`Agent`, and any non-priming `Bash` until you actually prime.
30
+ >
31
+ > - **Stage 1 (armed):** only the read-only priming steps (version check, team-repo probe,
32
+ > `knowledge.js manifest`, the developer interview) and `knowledge.js kickoff-preflight`
33
+ > are permitted. Running **preflight (Step 3) does NOT release the gate — it advances it to
34
+ > stage 2.**
35
+ > - **Stage 2 (primer):** the load-set is resolved but the **team knowledge base is not yet
36
+ > primed.** Everything stays blocked until you spawn the **`context-primer` subagent in the
37
+ > foreground** (Step 4) — priming the team KB via that subagent is **what releases the gate.**
38
+ > Reading `repo-path-<repo>` memories and asking the developer stay allowed.
39
+ >
40
+ > So the correct response to a block message is never to fight it: run Steps 0–3, then
41
+ > **prime the team KB via the foreground `context-primer` subagent (Step 4) BEFORE any other
42
+ > research** — before you open a single repo file, even one named in the `/kickoff` sentence.
43
+ > (Emergency override only: `KICKOFF_GATE_DISABLED=1`.)
35
44
 
36
45
  ## Session permission policy — auto-accept local file I/O, ALWAYS confirm dangerous execution
37
46
 
@@ -257,20 +266,30 @@ source — this is the one thing preflight can't know):
257
266
 
258
267
  Never ask for a repo not in `loadSet`.
259
268
 
260
- ## Step 4 — Load the knowledge via the `context-primer` subagent (do NOT read docs inline)
261
-
262
- **This is the token-discipline core of kickoff.** Reading every architecture + feature +
263
- standard + client doc *into this conversation* is exactly what bloats the thread and trips
264
- compaction later. So you do **not** read those docs here. Instead, **delegate the heavy
265
- reading to the `context-primer` subagent**, which reads them in *its own* context and
266
- returns a small distilled briefing. Preflight (Step 3) has already released the kickoff
267
- gate, so spawning a subagent is now permitted.
268
-
269
- Spawn it with the `Agent` tool (`subagent_type: context-primer`) and pass:
270
- - `TEAM_REPO` (the resolved path),
271
- - the **full `kickoff-preflight` JSON** from Step 3 (it contains `reads[]`, `clientScope`,
272
- `standards`, `client`, `estimate`),
273
- - the developer's one-line task description.
269
+ ## Step 4 — Prime the team knowledge base FIRST, via the `context-primer` subagent
270
+
271
+ > 🛑 **THIS IS THE FIRST RESEARCH YOU DO — NOT OPTIONAL, NOT PARALLELIZABLE.**
272
+ > After the interview, the team knowledge base is the **first source you prime from, before
273
+ > ANY other research.** You may **not** read repo code, `Grep`/`Glob` the source, spawn any
274
+ > other agent, or open a file **named in the `/kickoff` sentence** until the `context-primer`
275
+ > subagent has **returned its briefing.** Preflight (Step 3) did **not** release the gate — it
276
+ > advanced it to stage 2, and **spawning the foreground `context-primer` is what releases it.**
277
+ > The team KB is the team's distilled, shared gold: skipping it, or side-channeling it (running
278
+ > the primer in the background while you read code in parallel), is the exact failure this gate
279
+ > exists to prevent — do neither.
280
+
281
+ **This is also the token-discipline core of kickoff.** Reading every architecture + feature +
282
+ standard + client doc *into this conversation* bloats the thread and trips compaction later. So
283
+ you do **not** read those docs here — you **delegate the heavy reading to the `context-primer`
284
+ subagent**, which reads them in *its own* context and returns a small distilled briefing.
285
+
286
+ Spawn it with the `Agent` tool and **wait for it**:
287
+ - `subagent_type: context-primer`,
288
+ - **`run_in_background: false`** — foreground, so you MUST wait for the briefing before doing
289
+ anything else; a backgrounded primer running while you read code is precisely the violation,
290
+ - pass it: `TEAM_REPO` (the resolved path), the **full `kickoff-preflight` JSON** from Step 3
291
+ (`reads[]`, `clientScope`, `standards`, `client`, `estimate`), and the developer's one-line
292
+ task description.
274
293
 
275
294
  The primer returns a `## Primer` block (critical rules, task-relevant knowledge + gotchas,
276
295
  client variations, gaps) plus a `## Doc map`. **Keep only that briefing as your context for