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.
- package/.claude/settings.json +1 -1
- package/knowledge/2.0/apps/_underscore/features/error-reporting-issue-event.md +44 -12
- package/knowledge/sessions/2026-08-12-api2-error-capture-schema-drift-jcardinal.md +87 -0
- package/package.json +1 -1
- package/scripts/hooks/kickoff-gate.js +146 -52
- package/skills/kickoff/SKILL.md +40 -21
package/.claude/settings.json
CHANGED
|
@@ -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-
|
|
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
|
-
##
|
|
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
|
-
**
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
**Cause:** `
|
|
453
|
-
|
|
454
|
-
|
|
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
|
@@ -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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
-
*
|
|
30
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
/*
|
|
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
|
|
130
|
+
function armedMessage() {
|
|
104
131
|
return [
|
|
105
|
-
'🛑 KICKOFF GATE — priming
|
|
132
|
+
'🛑 KICKOFF GATE (stage 1 of 2) — priming has not started.',
|
|
106
133
|
'',
|
|
107
|
-
'/kickoff was invoked this session.
|
|
108
|
-
'
|
|
109
|
-
'
|
|
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
|
-
'
|
|
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 ...
|
|
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
|
-
'
|
|
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
|
|
140
|
-
'
|
|
141
|
-
'
|
|
142
|
-
'
|
|
143
|
-
'
|
|
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
|
|
223
|
+
const toolInput = data.tool_input || {};
|
|
224
|
+
const command = toolInput.command || '';
|
|
225
|
+
const phase = lock.phase || 'armed';
|
|
162
226
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
package/skills/kickoff/SKILL.md
CHANGED
|
@@ -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.
|
|
30
|
-
>
|
|
31
|
-
>
|
|
32
|
-
>
|
|
33
|
-
>
|
|
34
|
-
>
|
|
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 —
|
|
261
|
-
|
|
262
|
-
**
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|