create-claude-cabinet 0.21.0 → 0.22.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/README.md +1 -0
- package/package.json +1 -1
- package/templates/briefing/_briefing-work-tracking-template.md +20 -0
- package/templates/cabinet/pib-db-access.md +26 -12
- package/templates/cabinet/pib-db-triggers.md +192 -0
- package/templates/scripts/pib-db-lib.mjs +187 -14
- package/templates/scripts/pib-db-mcp-server.mjs +52 -8
- package/templates/scripts/pib-db-schema.sql +40 -25
- package/templates/scripts/pib-db.mjs +35 -0
- package/templates/scripts/review-ui.html +71 -4
- package/templates/scripts/work-tracker-server.mjs +32 -0
- package/templates/scripts/work-tracker-ui.html +287 -5
- package/templates/skills/orient/SKILL.md +39 -10
- package/templates/skills/orient/phases/deferred-check.md +55 -0
package/README.md
CHANGED
|
@@ -178,6 +178,7 @@ source code.
|
|
|
178
178
|
├── skills/ # orient, debrief, plan, execute, audit, etc.
|
|
179
179
|
│ └── cabinet-*/ # 31 cabinet member definitions
|
|
180
180
|
├── cabinet/ # committees, lifecycle, composition patterns
|
|
181
|
+
│ # (incl. pib-db-access.md, pib-db-triggers.md)
|
|
181
182
|
├── briefing/ # project briefing templates
|
|
182
183
|
├── hooks/ # git guardrails, telemetry
|
|
183
184
|
├── rules/ # enforcement pipeline
|
package/package.json
CHANGED
|
@@ -16,3 +16,23 @@ How this project tracks planned work. Skills that manage work items
|
|
|
16
16
|
*How to create, update, and close items.*
|
|
17
17
|
*Example: `POST /api/tasks` with JSON body*
|
|
18
18
|
*Example: `gh issue create --title "..." --body "..."`*
|
|
19
|
+
|
|
20
|
+
## Deferring work
|
|
21
|
+
|
|
22
|
+
*When to defer plainly vs. attach a trigger condition.*
|
|
23
|
+
|
|
24
|
+
Plain deferral (`status='deferred'` on actions, `status='someday'`
|
|
25
|
+
on projects) is for work that's blocked by something else you're
|
|
26
|
+
already tracking. It sits quietly until you unblock it yourself.
|
|
27
|
+
|
|
28
|
+
**Trigger-gated deferral** is for work waiting on an identifiable
|
|
29
|
+
external condition — a dependency landing, a stack decision
|
|
30
|
+
finalizing, a referenced file appearing. Every session, orient
|
|
31
|
+
re-evaluates each trigger against the current session's context
|
|
32
|
+
and surfaces items whose conditions have fired. Use
|
|
33
|
+
`pib_defer_with_trigger` (or `defer-with-trigger` CLI). See
|
|
34
|
+
`cabinet/pib-db-triggers.md` for the full convention.
|
|
35
|
+
|
|
36
|
+
Rule of thumb: if you can write one sentence describing what would
|
|
37
|
+
have to be true for this item to matter again, that sentence is
|
|
38
|
+
the trigger.
|
|
@@ -25,18 +25,32 @@ Check: are pib_* MCP tools available?
|
|
|
25
25
|
|
|
26
26
|
## Available Operations
|
|
27
27
|
|
|
28
|
-
| MCP Tool
|
|
29
|
-
|
|
|
30
|
-
| pib_create_project
|
|
31
|
-
| pib_list_projects
|
|
32
|
-
| pib_create_action
|
|
33
|
-
| pib_list_actions
|
|
34
|
-
| pib_update_action
|
|
35
|
-
| pib_complete_action
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
28
|
+
| MCP Tool | CLI Equivalent | Description |
|
|
29
|
+
| -------------------------- | ----------------------------------------------- | ------------------------------ |
|
|
30
|
+
| pib_create_project | create-project "name" --area X | Create a project |
|
|
31
|
+
| pib_list_projects | list-projects | List active projects |
|
|
32
|
+
| pib_create_action | create-action "text" --notes X | Create an action (work item) |
|
|
33
|
+
| pib_list_actions | list-actions [--status X] | List actions |
|
|
34
|
+
| pib_update_action | update-action fid --status X | Update action fields |
|
|
35
|
+
| pib_complete_action | complete-action fid | Mark action done |
|
|
36
|
+
| pib_defer_with_trigger | defer-with-trigger fid --trigger "<text>" | Defer with a return condition |
|
|
37
|
+
| pib_list_triggered | list-triggered [--include-done] | List items waiting on triggers |
|
|
38
|
+
| pib_mark_trigger_checked | mark-trigger-checked fid --result <value> | Record a trigger evaluation |
|
|
39
|
+
| pib_ingest_findings | ingest-findings run-dir | Ingest audit findings |
|
|
40
|
+
| pib_triage | triage finding-id status [notes] | Triage a finding |
|
|
41
|
+
| pib_triage_history | triage-history | Get suppression list |
|
|
42
|
+
| pib_query | query "SQL" | Run arbitrary SQL |
|
|
43
|
+
|
|
44
|
+
## Deferred triggers
|
|
45
|
+
|
|
46
|
+
When deferring an item that waits on a specific identifiable
|
|
47
|
+
condition — a dependency landing, a stack decision finalizing, a
|
|
48
|
+
referenced file appearing — use `pib_defer_with_trigger` instead of
|
|
49
|
+
plain `status='deferred'`. Orient re-evaluates each trigger every
|
|
50
|
+
session and surfaces items whose conditions have become true. See
|
|
51
|
+
[pib-db-triggers.md](pib-db-triggers.md) for the full convention:
|
|
52
|
+
result vocabulary, cascade semantics for projects, migration
|
|
53
|
+
guarantees, and known limitations.
|
|
40
54
|
|
|
41
55
|
## Surface Area Validation
|
|
42
56
|
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# pib-db Deferred Triggers
|
|
2
|
+
|
|
3
|
+
How to defer work items with structured return conditions and how
|
|
4
|
+
orient re-evaluates them each session.
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
|
|
8
|
+
Big ideas tend to rot. Someone raises an infrastructure proposal, a
|
|
9
|
+
platform-auth gap, or a stack-choice pivot — the idea is real, but it
|
|
10
|
+
cannot be acted on today. Without structure, it lands in `feedback/`
|
|
11
|
+
or a `status='deferred'` row and sits there forever. Nobody re-reads
|
|
12
|
+
`feedback/`. Nobody scans deferred items.
|
|
13
|
+
|
|
14
|
+
Deferred triggers turn those items into structurally-encoded work:
|
|
15
|
+
each one carries a natural-language condition describing what would
|
|
16
|
+
have to change to reactivate it. Orient surfaces them every session
|
|
17
|
+
and evaluates the condition against the current session's context.
|
|
18
|
+
The item sits in the queue, not on a reminder list.
|
|
19
|
+
|
|
20
|
+
## Schema
|
|
21
|
+
|
|
22
|
+
- `actions.trigger_condition TEXT` — natural-language predicate.
|
|
23
|
+
`NULL` means the action is not trigger-gated. Non-null means the
|
|
24
|
+
action is waiting on a specific condition.
|
|
25
|
+
- `projects.trigger_condition TEXT` — same semantics for projects.
|
|
26
|
+
- `trigger_checks` table — append-only history of evaluations.
|
|
27
|
+
Fields: `id`, `target_table`, `target_fid`, `checked_at`, `result`,
|
|
28
|
+
`notes`. No foreign key back to `actions`/`projects`, so history
|
|
29
|
+
is preserved even if the item is deleted.
|
|
30
|
+
|
|
31
|
+
## API
|
|
32
|
+
|
|
33
|
+
Three operations. Prefer MCP tools; fall back to CLI.
|
|
34
|
+
|
|
35
|
+
### `pib_defer_with_trigger(fid, triggerCondition, cascade?)`
|
|
36
|
+
|
|
37
|
+
Use when deferring with a specific return condition. Prefer over
|
|
38
|
+
`pib_update_action` with `status='deferred'` when the deferral is
|
|
39
|
+
conditional on something identifiable.
|
|
40
|
+
|
|
41
|
+
- Action: sets `status='deferred'` and writes `trigger_condition`.
|
|
42
|
+
- Project: sets `status='someday'` and writes `trigger_condition`.
|
|
43
|
+
- If a project has open child actions, `cascade: true` is required.
|
|
44
|
+
See [Cascade semantics](#cascade-semantics-for-projects).
|
|
45
|
+
|
|
46
|
+
CLI:
|
|
47
|
+
```bash
|
|
48
|
+
node scripts/pib-db.mjs defer-with-trigger <fid> --trigger "<text>" [--cascade]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### `pib_list_triggered({includeDone?})`
|
|
52
|
+
|
|
53
|
+
Returns items with `trigger_condition` set. By default excludes
|
|
54
|
+
completed items; pass `includeDone: true` to include them.
|
|
55
|
+
|
|
56
|
+
CLI:
|
|
57
|
+
```bash
|
|
58
|
+
node scripts/pib-db.mjs list-triggered [--include-done]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `pib_mark_trigger_checked(fid, result, notes?)`
|
|
62
|
+
|
|
63
|
+
Records an evaluation outcome into `trigger_checks`. Does not change
|
|
64
|
+
the item's `status` or `trigger_condition` — reactivation is a
|
|
65
|
+
separate explicit action taken by the user or orient.
|
|
66
|
+
|
|
67
|
+
CLI:
|
|
68
|
+
```bash
|
|
69
|
+
node scripts/pib-db.mjs mark-trigger-checked <fid> --result <value> [--notes "<text>"]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Result vocabulary
|
|
73
|
+
|
|
74
|
+
Four values. The CHECK constraint on `trigger_checks.result` rejects
|
|
75
|
+
anything else.
|
|
76
|
+
|
|
77
|
+
- `triggered` — the condition is now met; the item is ready to
|
|
78
|
+
reactivate. Surface in Attention Items. Do not auto-reopen —
|
|
79
|
+
leave the decision to the user.
|
|
80
|
+
- `still-waiting` — condition checked, not yet met. Normal idle state.
|
|
81
|
+
- `needs-info` — cannot evaluate from current session context. Flag
|
|
82
|
+
for the user; do not guess `triggered`. When in doubt, pick this.
|
|
83
|
+
- `condition-obsolete` — the condition no longer makes sense. Example:
|
|
84
|
+
"when we add Postgres support" but Postgres was dropped from the
|
|
85
|
+
roadmap. Triggers a review of whether to drop the item entirely or
|
|
86
|
+
rewrite the trigger.
|
|
87
|
+
|
|
88
|
+
## When to use vs plain deferred
|
|
89
|
+
|
|
90
|
+
| Situation | Mechanism |
|
|
91
|
+
|---|---|
|
|
92
|
+
| Blocked by something else on your plate right now | `status='deferred'` (no trigger) |
|
|
93
|
+
| Waiting for a specific external condition, needs monitoring | `trigger_condition` |
|
|
94
|
+
| Vague future intent, no specific signal | project `status='someday'` (no trigger) |
|
|
95
|
+
| "Someday, specifically when X happens" | project `trigger_condition` |
|
|
96
|
+
|
|
97
|
+
Rule of thumb: if you can write one sentence describing what would
|
|
98
|
+
have to be true for the item to matter again, that sentence is the
|
|
99
|
+
trigger. If you can't, it's plain deferred/someday.
|
|
100
|
+
|
|
101
|
+
## Cascade semantics for projects
|
|
102
|
+
|
|
103
|
+
Deferring a project with open child actions requires `cascade: true`.
|
|
104
|
+
The cascade:
|
|
105
|
+
|
|
106
|
+
1. Sets each open child action to `status='deferred'`.
|
|
107
|
+
2. Appends an inheritance line to each child's notes:
|
|
108
|
+
`_Deferred alongside parent prj:abc (trigger: <text>)_`
|
|
109
|
+
3. Does NOT write `trigger_condition` to children. The parent's
|
|
110
|
+
trigger is the single source of truth.
|
|
111
|
+
|
|
112
|
+
When the parent reopens (user flips status back to `active`, or
|
|
113
|
+
orient surfaces it as `triggered` and the user accepts):
|
|
114
|
+
|
|
115
|
+
- Children remain `deferred`.
|
|
116
|
+
- The user decides which children to reopen. Reopening everything
|
|
117
|
+
automatically often resurrects stale subgoals that the deferral
|
|
118
|
+
period should have retired.
|
|
119
|
+
|
|
120
|
+
## Orient integration
|
|
121
|
+
|
|
122
|
+
The `deferred-check.md` phase fires after `work-scan.md`, before
|
|
123
|
+
`auto-maintenance.md`. Every session:
|
|
124
|
+
|
|
125
|
+
1. Orient calls `pib_list_triggered` (or CLI fallback).
|
|
126
|
+
2. For each item, evaluates the trigger text against the current
|
|
127
|
+
session's context (new dependencies installed? a referenced file
|
|
128
|
+
now exists? a stack decision was finalized?).
|
|
129
|
+
3. Records each evaluation via `pib_mark_trigger_checked`. Prefer
|
|
130
|
+
`needs-info` over guessing `triggered`.
|
|
131
|
+
4. Items that evaluate to `triggered` appear in the briefing's
|
|
132
|
+
**Attention Items** section. Orient does not auto-reopen — the
|
|
133
|
+
user decides.
|
|
134
|
+
|
|
135
|
+
Cost control: cap the phase at 30 seconds. If more than 10 items are
|
|
136
|
+
triggered, evaluate only the 5 least-recently-checked.
|
|
137
|
+
|
|
138
|
+
## Migration guarantees
|
|
139
|
+
|
|
140
|
+
- **Additive-only.** New columns and new tables are allowed through
|
|
141
|
+
the `migrate()` path. Destructive changes (dropping columns,
|
|
142
|
+
changing types, removing constraints) require a new versioned
|
|
143
|
+
migration with explicit approval — they do not belong in the
|
|
144
|
+
default path.
|
|
145
|
+
- **Gated on every startup.** Migrations run on every MCP startup
|
|
146
|
+
and every CLI invocation, gated by `PRAGMA user_version`. Only
|
|
147
|
+
pending migrations apply; the path is idempotent.
|
|
148
|
+
- **Per-worktree.** Each worktree's local `pib.db` migrates
|
|
149
|
+
independently. Migrations run against whichever DB the process
|
|
150
|
+
opens. This is normal SQLite worktree behavior.
|
|
151
|
+
- **Schema parity invariant.** Any new `trigger_*` column added to
|
|
152
|
+
`actions` MUST also be added to `projects` in the same migration.
|
|
153
|
+
The two tables mirror each other for trigger semantics.
|
|
154
|
+
|
|
155
|
+
## Index placement rule
|
|
156
|
+
|
|
157
|
+
If any future index references a column added through the `migrate()`
|
|
158
|
+
path, the index must also be created via `migrate()`, NOT in the
|
|
159
|
+
`SCHEMA` block's `CREATE INDEX IF NOT EXISTS` stanza.
|
|
160
|
+
|
|
161
|
+
Reason: the `SCHEMA` block runs before ALTER TABLE migrations.
|
|
162
|
+
Indexing a yet-to-exist column errors on existing databases. The
|
|
163
|
+
rule is mechanical — add the column in migrate(), add the index in
|
|
164
|
+
migrate(), in that order.
|
|
165
|
+
|
|
166
|
+
## CLI equivalents
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
node scripts/pib-db.mjs defer-with-trigger <fid> --trigger "<text>" [--cascade]
|
|
170
|
+
node scripts/pib-db.mjs list-triggered [--include-done]
|
|
171
|
+
node scripts/pib-db.mjs mark-trigger-checked <fid> --result <value> [--notes "<text>"]
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
All three map 1:1 to the library functions in `pib-db-lib.mjs`. Use
|
|
175
|
+
MCP tools when available; the CLI is the fallback for non-MCP
|
|
176
|
+
contexts.
|
|
177
|
+
|
|
178
|
+
## Known limitations
|
|
179
|
+
|
|
180
|
+
- **Trigger evaluation is LLM-semantic, not deterministic.** Two
|
|
181
|
+
sessions may evaluate the same trigger differently. The
|
|
182
|
+
`trigger_checks` history is the audit trail — read it to see how
|
|
183
|
+
past sessions interpreted the same condition.
|
|
184
|
+
- **No forcing function between sessions.** If a trigger sits
|
|
185
|
+
unchecked across many sessions (no orient runs), nothing forces
|
|
186
|
+
evaluation. Worst case: a `triggered` item sits unnoticed for a
|
|
187
|
+
week. Acceptable soft limit; the alternative (scheduled jobs) adds
|
|
188
|
+
infrastructure we don't want.
|
|
189
|
+
- **Concurrent migration race, theoretical.** SQLite handles locking,
|
|
190
|
+
but a race on `PRAGMA user_version` is theoretically possible if
|
|
191
|
+
two MCP processes open the DB at the same instant. In practice,
|
|
192
|
+
MCP servers are per-session and the window is microseconds.
|
|
@@ -23,24 +23,68 @@ function today() {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
|
-
//
|
|
26
|
+
// Migrations — gated by PRAGMA user_version
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// SCHEMA_VERSION history:
|
|
29
|
+
// 1 — added actions.status CHECK constraint
|
|
30
|
+
// 2 — added actions.tags
|
|
31
|
+
// 3 — added trigger_condition on actions/projects + trigger_checks history
|
|
32
|
+
// 4 — composite index on trigger_checks(target_fid, checked_at DESC) for listTriggered
|
|
33
|
+
export const SCHEMA_VERSION = 4;
|
|
34
|
+
|
|
35
|
+
// Each entry: { version, sql }. A single version may have multiple SQL
|
|
36
|
+
// statements (e.g. column add + index). Statements run in array order;
|
|
37
|
+
// each is wrapped in try/catch so re-running on a DB that already has
|
|
38
|
+
// the column/table is a no-op. The user_version pragma is the primary
|
|
39
|
+
// gate — try/catch is a safety net for pre-pragma DBs.
|
|
40
|
+
const MIGRATIONS = [
|
|
41
|
+
{ version: 1, sql: "ALTER TABLE actions ADD COLUMN status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','in-progress','blocked','deferred','done'))" },
|
|
42
|
+
{ version: 2, sql: "ALTER TABLE actions ADD COLUMN tags TEXT NOT NULL DEFAULT ''" },
|
|
43
|
+
{ version: 3, sql: "ALTER TABLE actions ADD COLUMN trigger_condition TEXT" },
|
|
44
|
+
{ version: 3, sql: "ALTER TABLE projects ADD COLUMN trigger_condition TEXT" },
|
|
45
|
+
{ version: 3, sql: `CREATE TABLE IF NOT EXISTS trigger_checks (
|
|
46
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
47
|
+
target_table TEXT NOT NULL CHECK(target_table IN ('actions','projects')),
|
|
48
|
+
target_fid TEXT NOT NULL,
|
|
49
|
+
checked_at TEXT NOT NULL,
|
|
50
|
+
result TEXT NOT NULL CHECK(result IN ('triggered','still-waiting','needs-info','condition-obsolete')),
|
|
51
|
+
notes TEXT
|
|
52
|
+
)` },
|
|
53
|
+
{ version: 3, sql: "CREATE INDEX IF NOT EXISTS idx_trigger_checks_fid ON trigger_checks(target_fid)" },
|
|
54
|
+
{ version: 4, sql: "CREATE INDEX IF NOT EXISTS idx_trigger_checks_target_time ON trigger_checks(target_fid, checked_at DESC)" },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
export function migrate(db) {
|
|
58
|
+
const current = db.pragma('user_version', { simple: true });
|
|
59
|
+
if (current >= SCHEMA_VERSION) return { from: current, to: current, applied: 0 };
|
|
60
|
+
|
|
61
|
+
// Wrap in a transaction so a real mid-migration failure (disk full,
|
|
62
|
+
// locked DB, constraint violation) rolls back user_version along with
|
|
63
|
+
// the partial DDL. Only swallow "already exists" errors from legacy
|
|
64
|
+
// pre-pragma DBs where columns may have been added before versioning.
|
|
65
|
+
const tx = db.transaction(() => {
|
|
66
|
+
let applied = 0;
|
|
67
|
+
for (const m of MIGRATIONS) {
|
|
68
|
+
if (m.version <= current) continue;
|
|
69
|
+
try { db.exec(m.sql); applied++; }
|
|
70
|
+
catch (e) {
|
|
71
|
+
if (!/already exists|duplicate column/i.test(e.message || '')) throw e;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
db.pragma(`user_version = ${SCHEMA_VERSION}`);
|
|
75
|
+
return applied;
|
|
76
|
+
});
|
|
77
|
+
const applied = tx();
|
|
78
|
+
return { from: current, to: SCHEMA_VERSION, applied };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Init — create tables from schema, then migrate
|
|
27
83
|
// ---------------------------------------------------------------------------
|
|
28
84
|
export function init(db, { schemaPath }) {
|
|
29
85
|
const schema = readFileSync(schemaPath, 'utf-8');
|
|
30
86
|
db.exec(schema);
|
|
31
|
-
|
|
32
|
-
// Migrate existing DBs — add columns that may not exist yet
|
|
33
|
-
const migrations = [
|
|
34
|
-
{ table: 'actions', column: 'status', sql: "ALTER TABLE actions ADD COLUMN status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','in-progress','blocked','deferred','done'))" },
|
|
35
|
-
{ table: 'actions', column: 'tags', sql: "ALTER TABLE actions ADD COLUMN tags TEXT NOT NULL DEFAULT ''" },
|
|
36
|
-
];
|
|
37
|
-
for (const m of migrations) {
|
|
38
|
-
const cols = db.prepare(`PRAGMA table_info(${m.table})`).all();
|
|
39
|
-
if (!cols.some(c => c.name === m.column)) {
|
|
40
|
-
try { db.exec(m.sql); } catch { /* column may already exist */ }
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
87
|
+
migrate(db);
|
|
44
88
|
return { message: `Database initialized` };
|
|
45
89
|
}
|
|
46
90
|
|
|
@@ -291,3 +335,132 @@ export function triageHistory(db) {
|
|
|
291
335
|
deferredFingerprints: deferred.map(r => ({ 'cabinet-member': r.cabinet_member, title: r.title })),
|
|
292
336
|
};
|
|
293
337
|
}
|
|
338
|
+
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// Deferred triggers
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// Items (actions or projects) with a trigger_condition are waiting on a
|
|
343
|
+
// specific condition. The orient skill re-evaluates them each session and
|
|
344
|
+
// records each check in trigger_checks (append-only history).
|
|
345
|
+
|
|
346
|
+
export const TRIGGER_RESULT_VOCABULARY = ['triggered', 'still-waiting', 'needs-info', 'condition-obsolete'];
|
|
347
|
+
export const FID_PATTERN = /^(act|prj):[a-f0-9]{8}$/;
|
|
348
|
+
const TRIGGER_CONDITION_MAX_LENGTH = 2000;
|
|
349
|
+
|
|
350
|
+
function validateFid(fid) {
|
|
351
|
+
if (!fid || typeof fid !== 'string') {
|
|
352
|
+
return { error: 'missing_fid', message: 'fid is required' };
|
|
353
|
+
}
|
|
354
|
+
if (!FID_PATTERN.test(fid)) {
|
|
355
|
+
return { error: 'invalid_fid_format', message: `fid must match ${FID_PATTERN}, got "${fid}"` };
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function tableForFid(fid) {
|
|
361
|
+
return fid.startsWith('prj:') ? 'projects' : 'actions';
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function deferWithTrigger(db, { fid, triggerCondition, cascade = false } = {}) {
|
|
365
|
+
const fidError = validateFid(fid);
|
|
366
|
+
if (fidError) return { error: fidError };
|
|
367
|
+
if (!triggerCondition || typeof triggerCondition !== 'string' || triggerCondition.trim() === '') {
|
|
368
|
+
return { error: { error: 'missing_trigger_condition', message: 'triggerCondition must be a non-empty string' } };
|
|
369
|
+
}
|
|
370
|
+
if (triggerCondition.length > TRIGGER_CONDITION_MAX_LENGTH) {
|
|
371
|
+
return { error: { error: 'trigger_condition_too_long', message: `triggerCondition must be ≤${TRIGGER_CONDITION_MAX_LENGTH} chars, got ${triggerCondition.length}` } };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const table = tableForFid(fid);
|
|
375
|
+
const row = db.prepare(`SELECT status, ${table === 'actions' ? 'completed' : "'0' as completed"} FROM ${table} WHERE fid = ? AND deleted_at IS NULL`).get(fid);
|
|
376
|
+
if (!row) return { error: { error: 'not_found', message: `No ${table} row with fid ${fid}` } };
|
|
377
|
+
if (row.status === 'done' || row.completed === 1) {
|
|
378
|
+
return { error: { error: 'already_done', message: `${fid} is already done; cannot defer` } };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let cascaded = 0;
|
|
382
|
+
if (table === 'projects') {
|
|
383
|
+
// Children with their own trigger_condition already carry their own
|
|
384
|
+
// return condition; cascade leaves them alone so the parent's trigger
|
|
385
|
+
// doesn't overwrite their independent wait state.
|
|
386
|
+
const openChildren = db.prepare(`SELECT fid FROM actions WHERE project_fid = ? AND status NOT IN ('done','deferred') AND trigger_condition IS NULL AND deleted_at IS NULL`).all(fid);
|
|
387
|
+
if (openChildren.length > 0 && !cascade) {
|
|
388
|
+
return {
|
|
389
|
+
error: {
|
|
390
|
+
error: 'has_open_children',
|
|
391
|
+
message: `Project ${fid} has ${openChildren.length} open action(s) without their own trigger. Pass cascade: true to defer them alongside.`,
|
|
392
|
+
openChildren: openChildren.map(c => c.fid),
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (cascade) {
|
|
397
|
+
const appendNote = `\n\n_Deferred alongside parent ${fid} (trigger: ${triggerCondition})_`;
|
|
398
|
+
const stmt = db.prepare(`UPDATE actions SET status = 'deferred', notes = notes || ? WHERE fid = ?`);
|
|
399
|
+
for (const child of openChildren) stmt.run(appendNote, child.fid);
|
|
400
|
+
cascaded = openChildren.length;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const newStatus = table === 'projects' ? 'someday' : 'deferred';
|
|
405
|
+
db.prepare(`UPDATE ${table} SET status = ?, trigger_condition = ? WHERE fid = ?`).run(newStatus, triggerCondition, fid);
|
|
406
|
+
|
|
407
|
+
return { fid, table, triggerCondition, status: newStatus, cascaded, message: `Deferred ${fid} with trigger` };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function listTriggered(db, { includeDone = false } = {}) {
|
|
411
|
+
const actionsWhere = includeDone
|
|
412
|
+
? 'a.trigger_condition IS NOT NULL AND a.deleted_at IS NULL'
|
|
413
|
+
: "a.trigger_condition IS NOT NULL AND a.deleted_at IS NULL AND a.status != 'done' AND (a.completed IS NULL OR a.completed = 0)";
|
|
414
|
+
const projectsWhere = includeDone
|
|
415
|
+
? 'p.trigger_condition IS NOT NULL AND p.deleted_at IS NULL'
|
|
416
|
+
: "p.trigger_condition IS NOT NULL AND p.deleted_at IS NULL AND p.status != 'done'";
|
|
417
|
+
|
|
418
|
+
const actions = db.prepare(`
|
|
419
|
+
SELECT a.fid, a.text, a.trigger_condition, a.status, p.name AS project_name,
|
|
420
|
+
(SELECT checked_at FROM trigger_checks WHERE target_fid = a.fid ORDER BY checked_at DESC LIMIT 1) AS last_checked,
|
|
421
|
+
(SELECT result FROM trigger_checks WHERE target_fid = a.fid ORDER BY checked_at DESC LIMIT 1) AS last_result
|
|
422
|
+
FROM actions a
|
|
423
|
+
LEFT JOIN projects p ON a.project_fid = p.fid
|
|
424
|
+
WHERE ${actionsWhere}
|
|
425
|
+
ORDER BY last_checked IS NOT NULL, last_checked ASC
|
|
426
|
+
`).all();
|
|
427
|
+
|
|
428
|
+
const projects = db.prepare(`
|
|
429
|
+
SELECT p.fid, p.name, p.trigger_condition, p.status,
|
|
430
|
+
(SELECT checked_at FROM trigger_checks WHERE target_fid = p.fid ORDER BY checked_at DESC LIMIT 1) AS last_checked,
|
|
431
|
+
(SELECT result FROM trigger_checks WHERE target_fid = p.fid ORDER BY checked_at DESC LIMIT 1) AS last_result
|
|
432
|
+
FROM projects p
|
|
433
|
+
WHERE ${projectsWhere}
|
|
434
|
+
ORDER BY last_checked IS NOT NULL, last_checked ASC
|
|
435
|
+
`).all();
|
|
436
|
+
|
|
437
|
+
return { actions, projects };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function markTriggerChecked(db, { fid, result, notes } = {}) {
|
|
441
|
+
const fidError = validateFid(fid);
|
|
442
|
+
if (fidError) return { error: fidError };
|
|
443
|
+
if (!TRIGGER_RESULT_VOCABULARY.includes(result)) {
|
|
444
|
+
return {
|
|
445
|
+
error: {
|
|
446
|
+
error: 'invalid_result',
|
|
447
|
+
message: `result must be one of: ${TRIGGER_RESULT_VOCABULARY.join(', ')}`,
|
|
448
|
+
got: result,
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
const table = tableForFid(fid);
|
|
453
|
+
const row = db.prepare(`SELECT status, ${table === 'actions' ? 'completed' : "'0' as completed"} FROM ${table} WHERE fid = ? AND deleted_at IS NULL`).get(fid);
|
|
454
|
+
if (!row) return { error: { error: 'not_found', message: `No ${table} row with fid ${fid}` } };
|
|
455
|
+
if (row.status === 'done' || row.completed === 1) {
|
|
456
|
+
return { error: { error: 'already_done', message: `${fid} is already done; recording a trigger check on a completed item is not allowed` } };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const checkedAt = new Date().toISOString();
|
|
460
|
+
db.prepare(`
|
|
461
|
+
INSERT INTO trigger_checks (target_table, target_fid, checked_at, result, notes)
|
|
462
|
+
VALUES (?, ?, ?, ?, ?)
|
|
463
|
+
`).run(table, fid, checkedAt, result, notes || null);
|
|
464
|
+
|
|
465
|
+
return { fid, checkedAt, result, message: `Recorded trigger check for ${fid}: ${result}` };
|
|
466
|
+
}
|
|
@@ -71,6 +71,12 @@ function getDb() {
|
|
|
71
71
|
db = new Database(DB_PATH);
|
|
72
72
|
db.pragma('journal_mode = WAL');
|
|
73
73
|
db.pragma('foreign_keys = ON');
|
|
74
|
+
|
|
75
|
+
// Ensure schema exists, then apply pending migrations. Both are cheap:
|
|
76
|
+
// the schema file is idempotent (CREATE TABLE IF NOT EXISTS) and migrate()
|
|
77
|
+
// short-circuits when PRAGMA user_version is already current.
|
|
78
|
+
const schemaPath = join(__dirname, 'pib-db-schema.sql');
|
|
79
|
+
lib.init(db, { schemaPath });
|
|
74
80
|
return db;
|
|
75
81
|
}
|
|
76
82
|
|
|
@@ -191,6 +197,46 @@ const TOOLS = [
|
|
|
191
197
|
description: 'Get the triage suppression list (rejected and deferred findings).',
|
|
192
198
|
inputSchema: { type: 'object', properties: {} },
|
|
193
199
|
},
|
|
200
|
+
{
|
|
201
|
+
name: 'pib_defer_with_trigger',
|
|
202
|
+
description: 'Defer an action or project with a free-text trigger condition describing what would reactivate it. Orient re-evaluates triggers each session. Use this INSTEAD of pib_update_action with status=deferred when the deferral is waiting for a specific condition.',
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: 'object',
|
|
205
|
+
properties: {
|
|
206
|
+
fid: { type: 'string', description: 'Action or project fid (e.g., act:abc12345 or prj:abc12345)' },
|
|
207
|
+
triggerCondition: { type: 'string', description: 'Natural-language predicate describing when to reactivate (e.g., "3+ projects calling Claude API directly")' },
|
|
208
|
+
cascade: { type: 'boolean', description: 'When deferring a project with open child actions, also defer them. Required for projects with open children.' },
|
|
209
|
+
},
|
|
210
|
+
required: ['fid', 'triggerCondition'],
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'pib_list_triggered',
|
|
215
|
+
description: 'List all items (actions and projects) with an active trigger_condition. Returns two arrays: actions and projects, each with fid, trigger text, last check result, and days since last check.',
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
properties: {
|
|
219
|
+
includeDone: { type: 'boolean', description: 'Include items already marked done. Default false.' },
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'pib_mark_trigger_checked',
|
|
225
|
+
description: 'Record that a trigger was evaluated. Use after orient\'s deferred-check phase evaluates triggers against session context. Result must be one of: triggered | still-waiting | needs-info | condition-obsolete.',
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: {
|
|
229
|
+
fid: { type: 'string', description: 'Action or project fid' },
|
|
230
|
+
result: {
|
|
231
|
+
type: 'string',
|
|
232
|
+
enum: ['triggered', 'still-waiting', 'needs-info', 'condition-obsolete'],
|
|
233
|
+
description: 'Outcome of this evaluation',
|
|
234
|
+
},
|
|
235
|
+
notes: { type: 'string', description: 'Optional reasoning or context for the evaluation' },
|
|
236
|
+
},
|
|
237
|
+
required: ['fid', 'result'],
|
|
238
|
+
},
|
|
239
|
+
},
|
|
194
240
|
{
|
|
195
241
|
name: 'pib_query',
|
|
196
242
|
description: 'Run an arbitrary SQL query against the pib database.',
|
|
@@ -209,14 +255,6 @@ const TOOLS = [
|
|
|
209
255
|
// ---------------------------------------------------------------------------
|
|
210
256
|
function handleToolCall(name, args) {
|
|
211
257
|
const d = getDb();
|
|
212
|
-
const schemaPath = join(__dirname, 'pib-db-schema.sql');
|
|
213
|
-
|
|
214
|
-
// Auto-init: ensure tables exist
|
|
215
|
-
try {
|
|
216
|
-
d.prepare('SELECT 1 FROM projects LIMIT 0').run();
|
|
217
|
-
} catch {
|
|
218
|
-
lib.init(d, { schemaPath });
|
|
219
|
-
}
|
|
220
258
|
|
|
221
259
|
switch (name) {
|
|
222
260
|
case 'pib_create_project':
|
|
@@ -239,6 +277,12 @@ function handleToolCall(name, args) {
|
|
|
239
277
|
return lib.triage(d, args);
|
|
240
278
|
case 'pib_triage_history':
|
|
241
279
|
return lib.triageHistory(d);
|
|
280
|
+
case 'pib_defer_with_trigger':
|
|
281
|
+
return lib.deferWithTrigger(d, args);
|
|
282
|
+
case 'pib_list_triggered':
|
|
283
|
+
return lib.listTriggered(d, args);
|
|
284
|
+
case 'pib_mark_trigger_checked':
|
|
285
|
+
return lib.markTriggerChecked(d, args);
|
|
242
286
|
case 'pib_query':
|
|
243
287
|
return lib.query(d, args);
|
|
244
288
|
default:
|
|
@@ -7,34 +7,36 @@
|
|
|
7
7
|
-- Query: node scripts/pib-db.mjs query "SELECT ..."
|
|
8
8
|
|
|
9
9
|
CREATE TABLE IF NOT EXISTS projects (
|
|
10
|
-
fid
|
|
11
|
-
name
|
|
12
|
-
area
|
|
13
|
-
status
|
|
14
|
-
|
|
15
|
-
notes
|
|
16
|
-
created
|
|
17
|
-
completed_at
|
|
18
|
-
due
|
|
19
|
-
deleted_at
|
|
10
|
+
fid TEXT PRIMARY KEY CHECK(fid GLOB 'prj:*'),
|
|
11
|
+
name TEXT NOT NULL,
|
|
12
|
+
area TEXT,
|
|
13
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
14
|
+
CHECK(status IN ('active','paused','done','dropped','someday')),
|
|
15
|
+
notes TEXT NOT NULL DEFAULT '',
|
|
16
|
+
created TEXT NOT NULL CHECK(created GLOB '????-??-??'),
|
|
17
|
+
completed_at TEXT,
|
|
18
|
+
due TEXT,
|
|
19
|
+
deleted_at TEXT,
|
|
20
|
+
trigger_condition TEXT
|
|
20
21
|
);
|
|
21
22
|
|
|
22
23
|
CREATE TABLE IF NOT EXISTS actions (
|
|
23
|
-
fid
|
|
24
|
-
text
|
|
25
|
-
area
|
|
26
|
-
project_fid
|
|
27
|
-
due
|
|
28
|
-
flagged
|
|
29
|
-
completed
|
|
30
|
-
completed_at
|
|
31
|
-
status
|
|
32
|
-
|
|
33
|
-
tags
|
|
34
|
-
sort_order
|
|
35
|
-
created
|
|
36
|
-
notes
|
|
37
|
-
deleted_at
|
|
24
|
+
fid TEXT PRIMARY KEY CHECK(fid GLOB 'act:*'),
|
|
25
|
+
text TEXT NOT NULL,
|
|
26
|
+
area TEXT,
|
|
27
|
+
project_fid TEXT REFERENCES projects(fid) ON DELETE SET NULL,
|
|
28
|
+
due TEXT,
|
|
29
|
+
flagged INTEGER NOT NULL DEFAULT 0 CHECK(flagged IN (0, 1)),
|
|
30
|
+
completed INTEGER NOT NULL DEFAULT 0 CHECK(completed IN (0, 1)),
|
|
31
|
+
completed_at TEXT,
|
|
32
|
+
status TEXT NOT NULL DEFAULT 'open'
|
|
33
|
+
CHECK(status IN ('open','in-progress','blocked','deferred','done')),
|
|
34
|
+
tags TEXT NOT NULL DEFAULT '',
|
|
35
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
36
|
+
created TEXT NOT NULL CHECK(created GLOB '????-??-??'),
|
|
37
|
+
notes TEXT NOT NULL DEFAULT '',
|
|
38
|
+
deleted_at TEXT,
|
|
39
|
+
trigger_condition TEXT
|
|
38
40
|
);
|
|
39
41
|
|
|
40
42
|
CREATE TABLE IF NOT EXISTS audit_runs (
|
|
@@ -66,3 +68,16 @@ CREATE TABLE IF NOT EXISTS audit_findings (
|
|
|
66
68
|
triaged_at TEXT,
|
|
67
69
|
fix_description TEXT
|
|
68
70
|
);
|
|
71
|
+
|
|
72
|
+
-- Append-only history of trigger-condition evaluations.
|
|
73
|
+
-- No foreign key to actions/projects: if the target row is later deleted,
|
|
74
|
+
-- we want the historical record preserved (orphan rows are acceptable).
|
|
75
|
+
CREATE TABLE IF NOT EXISTS trigger_checks (
|
|
76
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
77
|
+
target_table TEXT NOT NULL CHECK(target_table IN ('actions','projects')),
|
|
78
|
+
target_fid TEXT NOT NULL,
|
|
79
|
+
checked_at TEXT NOT NULL,
|
|
80
|
+
result TEXT NOT NULL CHECK(result IN ('triggered','still-waiting','needs-info','condition-obsolete')),
|
|
81
|
+
notes TEXT
|
|
82
|
+
);
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_trigger_checks_fid ON trigger_checks(target_fid);
|