create-claude-cabinet 0.20.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/lib/omega-setup.js +71 -3
- 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/hooks/omega-memory-guard.sh +6 -8
- package/templates/rules/memory-capture.md +21 -0
- package/templates/scripts/pib-db-lib.mjs +187 -14
- package/templates/scripts/pib-db-mcp-server.mjs +96 -9
- 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/skill-validator.sh +1 -1
- package/templates/scripts/work-tracker-server.mjs +32 -0
- package/templates/scripts/work-tracker-ui.html +287 -5
- package/templates/skills/cc-feedback/SKILL.md +12 -17
- package/templates/skills/cc-publish/SKILL.md +8 -3
- package/templates/skills/debrief/SKILL.md +22 -59
- package/templates/skills/debrief/phases/close-work.md +13 -2
- package/templates/skills/execute/SKILL.md +12 -2
- package/templates/skills/orient/SKILL.md +89 -10
- package/templates/skills/orient/phases/deferred-check.md +55 -0
- package/templates/skills/validate/SKILL.md +8 -2
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/lib/omega-setup.js
CHANGED
|
@@ -121,6 +121,39 @@ function setupOmega() {
|
|
|
121
121
|
results.push('Downloaded cross-encoder reranker model');
|
|
122
122
|
}
|
|
123
123
|
} catch { /* non-fatal */ }
|
|
124
|
+
|
|
125
|
+
// Ensure MCP server extra is installed (added in v0.21)
|
|
126
|
+
try {
|
|
127
|
+
execSync(`"${VENV_PYTHON}" -c "import mcp"`, { stdio: 'pipe' });
|
|
128
|
+
} catch {
|
|
129
|
+
console.log(' Installing MCP server support...');
|
|
130
|
+
try {
|
|
131
|
+
execSync(`"${VENV_PYTHON}" -m pip install --quiet "omega-memory[server]"`, {
|
|
132
|
+
stdio: 'pipe', timeout: 120000,
|
|
133
|
+
});
|
|
134
|
+
results.push('Installed MCP server support');
|
|
135
|
+
} catch { /* non-fatal */ }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Ensure omega MCP server is registered in global settings
|
|
139
|
+
try {
|
|
140
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
141
|
+
let settings = {};
|
|
142
|
+
if (fs.existsSync(settingsPath)) {
|
|
143
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch { settings = {}; }
|
|
144
|
+
}
|
|
145
|
+
if (!settings.mcpServers) settings.mcpServers = {};
|
|
146
|
+
if (!settings.mcpServers.omega) {
|
|
147
|
+
settings.mcpServers.omega = {
|
|
148
|
+
command: path.join(VENV_DIR, 'bin', 'omega'),
|
|
149
|
+
args: ['serve'],
|
|
150
|
+
};
|
|
151
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
152
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
153
|
+
results.push('Registered omega MCP server (global settings)');
|
|
154
|
+
}
|
|
155
|
+
} catch { /* non-fatal */ }
|
|
156
|
+
|
|
124
157
|
return results;
|
|
125
158
|
} catch {
|
|
126
159
|
// Venv is broken — nuke and rebuild (D5)
|
|
@@ -133,13 +166,13 @@ function setupOmega() {
|
|
|
133
166
|
execSync(`"${pythonPath}" -m venv "${VENV_DIR}"`, { stdio: 'pipe' });
|
|
134
167
|
results.push('Created venv at ~/.claude-cabinet/omega-venv/');
|
|
135
168
|
|
|
136
|
-
// 4. Install omega-memory
|
|
169
|
+
// 4. Install omega-memory (with MCP server support)
|
|
137
170
|
console.log(' Installing omega-memory...');
|
|
138
|
-
execSync(`"${VENV_PYTHON}" -m pip install --quiet omega-memory`, {
|
|
171
|
+
execSync(`"${VENV_PYTHON}" -m pip install --quiet "omega-memory[server]"`, {
|
|
139
172
|
stdio: 'pipe',
|
|
140
173
|
timeout: 120000,
|
|
141
174
|
});
|
|
142
|
-
results.push('Installed omega-memory');
|
|
175
|
+
results.push('Installed omega-memory (with MCP server)');
|
|
143
176
|
|
|
144
177
|
// 5. Download embedding model at install time (D4)
|
|
145
178
|
console.log(' Downloading embedding model...');
|
|
@@ -179,6 +212,41 @@ function setupOmega() {
|
|
|
179
212
|
results.push('Omega hooks setup skipped (run `omega hooks setup` manually)');
|
|
180
213
|
}
|
|
181
214
|
|
|
215
|
+
// 8. Register omega MCP server in global settings
|
|
216
|
+
// This enables omega_store(), omega_query(), etc. as MCP tools
|
|
217
|
+
// available to Claude Code directly (not just via hooks).
|
|
218
|
+
console.log(' Registering omega MCP server...');
|
|
219
|
+
try {
|
|
220
|
+
// Smoke-test: verify `omega serve` can start (requires mcp package)
|
|
221
|
+
execSync(
|
|
222
|
+
`echo '{}' | "${path.join(VENV_DIR, 'bin', 'omega')}" serve 2>&1 | head -1`,
|
|
223
|
+
{ stdio: 'pipe', timeout: 10000 }
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
227
|
+
let settings = {};
|
|
228
|
+
if (fs.existsSync(settingsPath)) {
|
|
229
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch { settings = {}; }
|
|
230
|
+
}
|
|
231
|
+
if (!settings.mcpServers) settings.mcpServers = {};
|
|
232
|
+
|
|
233
|
+
// Only add if not already configured
|
|
234
|
+
if (!settings.mcpServers.omega) {
|
|
235
|
+
settings.mcpServers.omega = {
|
|
236
|
+
command: path.join(VENV_DIR, 'bin', 'omega'),
|
|
237
|
+
args: ['serve'],
|
|
238
|
+
};
|
|
239
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
240
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
241
|
+
results.push('Registered omega MCP server (global settings)');
|
|
242
|
+
} else {
|
|
243
|
+
results.push('Omega MCP server already registered');
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
// Non-fatal — MCP tools are a convenience, hooks still work
|
|
247
|
+
results.push('Omega MCP server registration skipped (run `omega serve` manually to test)');
|
|
248
|
+
}
|
|
249
|
+
|
|
182
250
|
return results;
|
|
183
251
|
}
|
|
184
252
|
|
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.
|
|
@@ -27,14 +27,12 @@ if [ -z "$FILE_PATH" ]; then
|
|
|
27
27
|
fi
|
|
28
28
|
|
|
29
29
|
# Only care about memory directory paths
|
|
30
|
-
case
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
;;
|
|
37
|
-
esac
|
|
30
|
+
# Note: case patterns with * don't cross / boundaries in some shells,
|
|
31
|
+
# so we use [[ ]] substring matching for absolute path compatibility.
|
|
32
|
+
if [[ "$FILE_PATH" != *"/.claude/memory/"* ]] && [[ "$FILE_PATH" != *"/.claude/projects/"*"/memory/"* ]]; then
|
|
33
|
+
echo '{"decision":"allow"}'
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
38
36
|
|
|
39
37
|
# Allow MEMORY.md index files (structural, not memory content)
|
|
40
38
|
BASENAME=$(basename "$FILE_PATH")
|
|
@@ -56,3 +56,24 @@ verbally, or a constraint discovered through external research).
|
|
|
56
56
|
Over-capturing degrades retrieval quality. The test: *"Would a future
|
|
57
57
|
session benefit from knowing this?"* If yes, capture it. If it's just
|
|
58
58
|
noise or ephemera, skip it.
|
|
59
|
+
|
|
60
|
+
## Known Limitation: Auto-Memory System Prompt Conflict
|
|
61
|
+
|
|
62
|
+
Claude Code's built-in auto-memory system prompt describes a file-based
|
|
63
|
+
`.md` memory system (`/Users/<user>/.claude/projects/<project>/memory/`).
|
|
64
|
+
When omega is active, this conflicts — the system prompt tells Claude to
|
|
65
|
+
write `.md` files while CLAUDE.md and this rules file tell it to use
|
|
66
|
+
omega. The system prompt's instructions are strong and may override
|
|
67
|
+
project-level rules in some sessions.
|
|
68
|
+
|
|
69
|
+
**Mitigations:**
|
|
70
|
+
- The `omega-memory-guard` PreToolUse hook blocks flat markdown writes
|
|
71
|
+
when omega is available (structural enforcement, ~100% reliable)
|
|
72
|
+
- This rules file and CLAUDE.md omega instructions provide prompt-level
|
|
73
|
+
guidance (~80% reliable)
|
|
74
|
+
- If Claude creates a `.md` memory file despite these, the guard will
|
|
75
|
+
block it and redirect to `omega_store()`
|
|
76
|
+
|
|
77
|
+
This is a platform limitation — the auto-memory system prompt cannot
|
|
78
|
+
be suppressed from project configuration. The guard hook is the
|
|
79
|
+
primary defense.
|
|
@@ -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
|
+
}
|