create-claude-cabinet 0.45.0 → 0.47.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 +6 -10
- package/lib/cli.js +374 -91
- package/lib/copy.js +108 -6
- package/lib/db-setup.js +122 -17
- package/lib/engagement-server-setup.js +34 -9
- package/lib/metadata.js +51 -2
- package/lib/migrate-from-omega.js +13 -1
- package/lib/mux-setup.js +33 -9
- package/lib/settings-merge.js +52 -10
- package/lib/watchtower-setup.js +230 -0
- package/package.json +5 -1
- package/templates/cabinet/_cabinet-member-template.md +8 -3
- package/templates/cabinet/advisories-state-schema.md +34 -7
- package/templates/cabinet/checklist-stats-schema.md +15 -1
- package/templates/cabinet/composition-patterns.md +4 -3
- package/templates/cabinet/memory-lifecycle-contract.md +135 -0
- package/templates/cabinet/pib-db-access.md +13 -0
- package/templates/cabinet/skill-output-conventions.md +35 -1
- package/templates/cabinet/watchtower-contracts.md +503 -1
- package/templates/cabinet/worktree-invocation-contract.md +87 -0
- package/templates/engagement/OVERVIEW.md +12 -0
- package/templates/engagement/__tests__/engagement.test.mjs +177 -2
- package/templates/engagement/__tests__/invoice-doc.test.mjs +195 -0
- package/templates/engagement/engagement-preview.mjs +100 -0
- package/templates/engagement/engagement-schema.md +240 -0
- package/templates/engagement/engagement.mjs +391 -11
- package/templates/engagement/pib-db-patches/pib-db-lib.mjs +10 -1
- package/templates/engagement/pib-db-patches/pib-db-mcp-server.mjs +5 -44
- package/templates/engagement/pib-db-patches/pib-db.mjs +5 -2
- package/templates/hooks/action-completion-gate.sh +9 -3
- package/templates/hooks/memory-index-guard.sh +17 -11
- package/templates/hooks/watchtower-session-start.sh +24 -2
- package/templates/mcp/pib-db.json +1 -4
- package/templates/mux/__tests__/mux-fail-loud.fixture.sh +44 -0
- package/templates/mux/__tests__/station-liveness.fixture.sh +234 -0
- package/templates/mux/__tests__/station-liveness.test.mjs +47 -0
- package/templates/mux/bin/mux +289 -56
- package/templates/mux/config/help.txt +1 -0
- package/templates/rules/acknowledge-when-corrected.md +33 -0
- package/templates/rules/maintainability.md +11 -0
- package/templates/rules/memory-capture.md +26 -4
- package/templates/rules/plan-before-bulk-or-irreversible-actions.md +48 -0
- package/templates/rules/verify-before-asserting.md +79 -0
- package/templates/scripts/__tests__/advisor-pass.test.mjs +238 -0
- package/templates/scripts/__tests__/advisories.test.mjs +262 -0
- package/templates/scripts/__tests__/ahead-check-origin.test.mjs +355 -0
- package/templates/scripts/__tests__/batch-disposition.test.mjs +235 -0
- package/templates/scripts/__tests__/claude-churn-authored.test.mjs +90 -0
- package/templates/scripts/__tests__/cross-ring-reader.test.mjs +505 -0
- package/templates/scripts/__tests__/dx-captures-briefing.test.mjs +169 -0
- package/templates/scripts/__tests__/feedback-outbox-flush.test.mjs +232 -0
- package/templates/scripts/__tests__/hook-runner.test.mjs +247 -0
- package/templates/scripts/__tests__/narrative-corpus.test.mjs +247 -0
- package/templates/scripts/__tests__/phase-shim.test.mjs +112 -0
- package/templates/scripts/__tests__/qa-handoff-gate.test.mjs +68 -0
- package/templates/scripts/__tests__/qa-handoff-merge-state.test.mjs +162 -0
- package/templates/scripts/__tests__/resolve-cli.test.mjs +314 -0
- package/templates/scripts/__tests__/ring-state-ownership.test.mjs +228 -3
- package/templates/scripts/__tests__/ring1-content-detector.test.mjs +168 -0
- package/templates/scripts/__tests__/ring1-flagged-actions.test.mjs +135 -0
- package/templates/scripts/__tests__/ring1-script-drift.test.mjs +126 -0
- package/templates/scripts/__tests__/ring2-queue-reader.test.mjs +80 -0
- package/templates/scripts/__tests__/ring2-recall-canary.test.mjs +150 -0
- package/templates/scripts/__tests__/ring2-roster-review.test.mjs +311 -0
- package/templates/scripts/__tests__/ring2-thread-context.test.mjs +189 -0
- package/templates/scripts/__tests__/ring3-chunk-merge.test.mjs +122 -0
- package/templates/scripts/__tests__/ring3-close-lenses.test.mjs +359 -0
- package/templates/scripts/__tests__/ring3-dedup.test.mjs +467 -0
- package/templates/scripts/__tests__/ring3-memory-titles.test.mjs +187 -0
- package/templates/scripts/__tests__/ring3-novelty-rescue.test.mjs +148 -0
- package/templates/scripts/__tests__/ring3-recent-slice.test.mjs +101 -0
- package/templates/scripts/__tests__/ring4-reconcile.test.mjs +393 -0
- package/templates/scripts/__tests__/routine-dispatch.test.mjs +312 -0
- package/templates/scripts/__tests__/shared-thread-reader.test.mjs +187 -0
- package/templates/scripts/__tests__/suppression-ledger.test.mjs +148 -0
- package/templates/scripts/__tests__/verify-backfill.test.mjs +103 -0
- package/templates/scripts/__tests__/verify-coverage.test.mjs +80 -0
- package/templates/scripts/__tests__/watchtower-snapshot.test.mjs +261 -0
- package/templates/scripts/__tests__/watchtower-sync.test.mjs +311 -0
- package/templates/scripts/load-triage-history.js +5 -2
- package/templates/scripts/pib-db-mcp-server.mjs +5 -44
- package/templates/scripts/pib-db-path.mjs +61 -0
- package/templates/scripts/pib-db.mjs +5 -2
- package/templates/scripts/validate-memory.mjs +214 -16
- package/templates/scripts/watchtower-advisories.mjs +309 -0
- package/templates/scripts/watchtower-build-context.mjs +301 -18
- package/templates/scripts/watchtower-cross-ring-reader.mjs +700 -0
- package/templates/scripts/watchtower-hook-runner.mjs +422 -0
- package/templates/scripts/watchtower-lib.mjs +447 -2
- package/templates/scripts/watchtower-narrative-corpus.mjs +385 -0
- package/templates/scripts/watchtower-phase-shim.mjs +171 -0
- package/templates/scripts/watchtower-queue.mjs +484 -1
- package/templates/scripts/watchtower-ring1.mjs +533 -53
- package/templates/scripts/watchtower-ring2.mjs +787 -48
- package/templates/scripts/watchtower-ring3-close.mjs +1357 -99
- package/templates/scripts/watchtower-ring4-runner.sh +85 -0
- package/templates/scripts/watchtower-ring4.mjs +753 -0
- package/templates/scripts/watchtower-routines.mjs +358 -0
- package/templates/scripts/watchtower-snapshot.mjs +452 -0
- package/templates/scripts/watchtower-status.sh +1 -1
- package/templates/scripts/watchtower-sync.mjs +393 -0
- package/templates/skills/audit/SKILL.md +5 -1
- package/templates/skills/briefing/SKILL.md +584 -236
- package/templates/skills/cabinet-anthropic-insider/SKILL.md +14 -6
- package/templates/skills/cabinet-historian/SKILL.md +14 -11
- package/templates/skills/cabinet-process-therapist/SKILL.md +28 -0
- package/templates/skills/cabinet-security/SKILL.md +11 -0
- package/templates/skills/cabinet-system-advocate/SKILL.md +22 -21
- package/templates/skills/cabinet-user-advocate/SKILL.md +13 -7
- package/templates/skills/catch-up/SKILL.md +113 -0
- package/templates/skills/cc-publish/SKILL.md +148 -25
- package/templates/skills/cc-remember/SKILL.md +45 -0
- package/templates/skills/close/SKILL.md +107 -0
- package/templates/skills/collab-client/SKILL.md +22 -5
- package/templates/skills/collab-consultant/SKILL.md +110 -2
- package/templates/skills/debrief/SKILL.md +148 -16
- package/templates/skills/debrief-classic/SKILL.md +696 -0
- package/templates/skills/debrief-classic/calibration.md +44 -0
- package/templates/skills/debrief-classic/phases/audit-pattern-capture.md +78 -0
- package/templates/skills/debrief-classic/phases/auto-maintenance.md +48 -0
- package/templates/skills/debrief-classic/phases/checklist-feedback.md +123 -0
- package/templates/skills/debrief-classic/phases/close-work.md +163 -0
- package/templates/skills/debrief-classic/phases/health-checks.md +54 -0
- package/templates/skills/debrief-classic/phases/inventory.md +40 -0
- package/templates/skills/debrief-classic/phases/loose-ends.md +52 -0
- package/templates/skills/debrief-classic/phases/methodology-capture.md +223 -0
- package/templates/skills/debrief-classic/phases/qa-handoff-sweep.md +78 -0
- package/templates/skills/debrief-classic/phases/record-lessons.md +177 -0
- package/templates/skills/debrief-classic/phases/report.md +59 -0
- package/templates/skills/debrief-classic/phases/update-state.md +48 -0
- package/templates/skills/debrief-classic/phases/upstream-feedback.md +185 -0
- package/templates/skills/debrief-classic/phases/verify-coverage.md +101 -0
- package/templates/skills/execute/SKILL.md +77 -6
- package/templates/skills/execute/phases/commit-and-deploy.md +8 -0
- package/templates/skills/execute-group/SKILL.md +23 -26
- package/templates/skills/generate-plan-groups/SKILL.md +20 -0
- package/templates/skills/inbox/SKILL.md +124 -7
- package/templates/skills/memory/SKILL.md +22 -6
- package/templates/skills/orient/SKILL.md +140 -52
- package/templates/skills/orient-classic/SKILL.md +770 -0
- package/templates/skills/orient-classic/phases/auto-maintenance.md +52 -0
- package/templates/skills/orient-classic/phases/briefing.md +53 -0
- package/templates/skills/orient-classic/phases/cabinet.md +46 -0
- package/templates/skills/orient-classic/phases/checklist-status.md +54 -0
- package/templates/skills/orient-classic/phases/context.md +88 -0
- package/templates/skills/orient-classic/phases/data-sync.md +35 -0
- package/templates/skills/orient-classic/phases/deferred-check.md +55 -0
- package/templates/skills/orient-classic/phases/dx-captures.md +53 -0
- package/templates/skills/orient-classic/phases/health-checks.md +50 -0
- package/templates/skills/orient-classic/phases/verify-backfill.md +109 -0
- package/templates/skills/orient-classic/phases/work-scan.md +69 -0
- package/templates/skills/plan/SKILL.md +8 -0
- package/templates/skills/qa-drain/SKILL.md +157 -0
- package/templates/skills/qa-handoff/SKILL.md +287 -46
- package/templates/skills/session-handoff/SKILL.md +175 -6
- package/templates/skills/setup-accounts/SKILL.md +38 -16
- package/templates/skills/threads/SKILL.md +4 -0
- package/templates/skills/triage-audit/SKILL.md +6 -0
- package/templates/skills/validate/phases/validators.md +41 -0
- package/templates/skills/watchtower/SKILL.md +199 -2
- package/templates/verify-runtime/CONVENTIONS.md +9 -0
- package/templates/verify-runtime/README.md +37 -0
- package/templates/watchtower/config.json.template +10 -2
- package/templates/watchtower/queue/items/item.json.schema +1 -1
- package/templates/workflows/execute-group-complete.js +10 -1
package/lib/settings-merge.js
CHANGED
|
@@ -8,7 +8,7 @@ const DEFAULT_HOOKS = {
|
|
|
8
8
|
hooks: [
|
|
9
9
|
{
|
|
10
10
|
type: 'command',
|
|
11
|
-
command: '
|
|
11
|
+
command: '$CLAUDE_PROJECT_DIR/.claude/hooks/git-guardrails.sh',
|
|
12
12
|
},
|
|
13
13
|
],
|
|
14
14
|
},
|
|
@@ -17,7 +17,7 @@ const DEFAULT_HOOKS = {
|
|
|
17
17
|
hooks: [
|
|
18
18
|
{
|
|
19
19
|
type: 'command',
|
|
20
|
-
command: '
|
|
20
|
+
command: '$CLAUDE_PROJECT_DIR/.claude/hooks/work-tracker-guard.sh',
|
|
21
21
|
},
|
|
22
22
|
],
|
|
23
23
|
},
|
|
@@ -26,7 +26,7 @@ const DEFAULT_HOOKS = {
|
|
|
26
26
|
hooks: [
|
|
27
27
|
{
|
|
28
28
|
type: 'command',
|
|
29
|
-
command: '
|
|
29
|
+
command: '$CLAUDE_PROJECT_DIR/.claude/hooks/cc-upstream-guard.sh',
|
|
30
30
|
},
|
|
31
31
|
],
|
|
32
32
|
},
|
|
@@ -35,7 +35,7 @@ const DEFAULT_HOOKS = {
|
|
|
35
35
|
hooks: [
|
|
36
36
|
{
|
|
37
37
|
type: 'command',
|
|
38
|
-
command: '
|
|
38
|
+
command: '$CLAUDE_PROJECT_DIR/.claude/hooks/action-quality-gate.sh',
|
|
39
39
|
},
|
|
40
40
|
],
|
|
41
41
|
},
|
|
@@ -44,7 +44,7 @@ const DEFAULT_HOOKS = {
|
|
|
44
44
|
hooks: [
|
|
45
45
|
{
|
|
46
46
|
type: 'command',
|
|
47
|
-
command: '
|
|
47
|
+
command: '$CLAUDE_PROJECT_DIR/.claude/hooks/action-completion-gate.sh',
|
|
48
48
|
},
|
|
49
49
|
],
|
|
50
50
|
},
|
|
@@ -55,7 +55,7 @@ const DEFAULT_HOOKS = {
|
|
|
55
55
|
hooks: [
|
|
56
56
|
{
|
|
57
57
|
type: 'command',
|
|
58
|
-
command: '
|
|
58
|
+
command: '$CLAUDE_PROJECT_DIR/.claude/hooks/skill-telemetry.sh',
|
|
59
59
|
},
|
|
60
60
|
],
|
|
61
61
|
},
|
|
@@ -66,7 +66,7 @@ const DEFAULT_HOOKS = {
|
|
|
66
66
|
hooks: [
|
|
67
67
|
{
|
|
68
68
|
type: 'command',
|
|
69
|
-
command: '
|
|
69
|
+
command: '$CLAUDE_PROJECT_DIR/.claude/hooks/skill-tool-telemetry.sh',
|
|
70
70
|
},
|
|
71
71
|
],
|
|
72
72
|
},
|
|
@@ -75,7 +75,7 @@ const DEFAULT_HOOKS = {
|
|
|
75
75
|
hooks: [
|
|
76
76
|
{
|
|
77
77
|
type: 'command',
|
|
78
|
-
command: '
|
|
78
|
+
command: '$CLAUDE_PROJECT_DIR/.claude/hooks/memory-index-guard.sh',
|
|
79
79
|
},
|
|
80
80
|
],
|
|
81
81
|
},
|
|
@@ -133,7 +133,7 @@ const BASH_COMPRESS_HOOKS = {
|
|
|
133
133
|
hooks: [
|
|
134
134
|
{
|
|
135
135
|
type: 'command',
|
|
136
|
-
command: '
|
|
136
|
+
command: '$CLAUDE_PROJECT_DIR/.claude/hooks/bash-output-compress.sh',
|
|
137
137
|
},
|
|
138
138
|
],
|
|
139
139
|
},
|
|
@@ -156,6 +156,38 @@ const LEGACY_HOOK_COMMANDS = [
|
|
|
156
156
|
'domain-memories.sh',
|
|
157
157
|
];
|
|
158
158
|
|
|
159
|
+
// CC-owned hook commands ship with a `$CLAUDE_PROJECT_DIR/` prefix so they
|
|
160
|
+
// resolve regardless of the hook's working directory. Older installs wrote them
|
|
161
|
+
// bare-relative (`.claude/hooks/X.sh`), which break ("No such file or
|
|
162
|
+
// directory") whenever a hook fires from a non-root cwd. bareForm() derives that
|
|
163
|
+
// legacy spelling so a reinstall can REPLACE it (drop old, add prefixed) rather
|
|
164
|
+
// than leave a broken duplicate.
|
|
165
|
+
function bareForm(command) {
|
|
166
|
+
return (command || '').replace(/^\$CLAUDE_PROJECT_DIR\//, '');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Every CC-owned hook command in a hook set, in its current spelling.
|
|
170
|
+
function ccHookCommands(hookSet) {
|
|
171
|
+
return Object.values(hookSet)
|
|
172
|
+
.flatMap(entries => entries.flatMap(e => (e.hooks || []).map(h => h.command)));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Remove every sub-hook whose command exactly matches one of `commands`, and
|
|
176
|
+
// prune entries left empty. Exact-match on CC-owned command strings only — a
|
|
177
|
+
// consumer's own hooks (different paths) are never touched.
|
|
178
|
+
function dropHookCommands(settings, commands) {
|
|
179
|
+
const drop = new Set(commands.filter(Boolean));
|
|
180
|
+
for (const [event, entries] of Object.entries(settings.hooks || {})) {
|
|
181
|
+
if (!Array.isArray(entries)) continue;
|
|
182
|
+
settings.hooks[event] = entries
|
|
183
|
+
.map(entry => (entry.hooks && Array.isArray(entry.hooks))
|
|
184
|
+
? { ...entry, hooks: entry.hooks.filter(h => !drop.has(h.command)) }
|
|
185
|
+
: entry)
|
|
186
|
+
.filter(entry => !entry.hooks || !Array.isArray(entry.hooks) || entry.hooks.length > 0);
|
|
187
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
159
191
|
/**
|
|
160
192
|
* Merge PIB hooks into the project's .claude/settings.json.
|
|
161
193
|
* Creates the file if it doesn't exist. Preserves existing hooks.
|
|
@@ -199,6 +231,13 @@ function mergeSettings(projectDir, { includeDb = true } = {}) {
|
|
|
199
231
|
if (!settings.hooks[event]) continue;
|
|
200
232
|
}
|
|
201
233
|
|
|
234
|
+
// Retire superseded bare-relative CC hooks so the $CLAUDE_PROJECT_DIR-prefixed
|
|
235
|
+
// forms below REPLACE them rather than duplicate. A bare `.claude/hooks/X.sh`
|
|
236
|
+
// only resolves when the hook runs from the project root; it errors ("No such
|
|
237
|
+
// file or directory") from any other cwd. Exact-match drop on CC-owned
|
|
238
|
+
// commands — consumer-authored hooks are left intact.
|
|
239
|
+
dropHookCommands(settings, ccHookCommands(DEFAULT_HOOKS).map(bareForm));
|
|
240
|
+
|
|
202
241
|
// Merge each hook event type from DEFAULT_HOOKS
|
|
203
242
|
for (const [event, newHooks] of Object.entries(DEFAULT_HOOKS)) {
|
|
204
243
|
if (!settings.hooks[event]) {
|
|
@@ -254,7 +293,7 @@ function healUserSettings() {
|
|
|
254
293
|
const filtered = entries.filter(entry => {
|
|
255
294
|
if (!entry.hooks || !Array.isArray(entry.hooks)) return true;
|
|
256
295
|
const hasRelativeCcHook = entry.hooks.some(h =>
|
|
257
|
-
|
|
296
|
+
/^(\$CLAUDE_PROJECT_DIR\/)?\.claude\/hooks\//.test(h.command || '')
|
|
258
297
|
);
|
|
259
298
|
if (hasRelativeCcHook) removed++;
|
|
260
299
|
return !hasRelativeCcHook;
|
|
@@ -338,6 +377,9 @@ function mergeBashCompressHooks(settingsPath) {
|
|
|
338
377
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
339
378
|
if (!settings.hooks) settings.hooks = {};
|
|
340
379
|
|
|
380
|
+
// Replace any superseded bare-relative form with the prefixed one (see mergeSettings).
|
|
381
|
+
dropHookCommands(settings, ccHookCommands(BASH_COMPRESS_HOOKS).map(bareForm));
|
|
382
|
+
|
|
341
383
|
for (const [event, newHooks] of Object.entries(BASH_COMPRESS_HOOKS)) {
|
|
342
384
|
if (!settings.hooks[event]) {
|
|
343
385
|
settings.hooks[event] = newHooks;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* watchtower-setup.js — keep the GLOBAL watchtower runtime fresh on reinstall.
|
|
3
|
+
*
|
|
4
|
+
* The watchtower module copies its files into the PROJECT (the normal module
|
|
5
|
+
* copy in lib/cli.js). But the GLOBAL runtime at ~/.claude-cabinet/watchtower/
|
|
6
|
+
* is only ever set up by the manual `/watchtower install` SKILL.md one-time
|
|
7
|
+
* step. So `npx create-claude-cabinet` (reinstall) refreshed the project files
|
|
8
|
+
* but NOT the global runtime scripts/docs/hooks — they went stale, and a
|
|
9
|
+
* brand-new runtime script (e.g. watchtower-advisories.mjs) never appeared
|
|
10
|
+
* until a hand-copy.
|
|
11
|
+
*
|
|
12
|
+
* This installer mirrors lib/mux-setup.js's content-aware refresh: a global
|
|
13
|
+
* SHA256 manifest at ~/.claude-cabinet/global-manifest.json records each
|
|
14
|
+
* dest's last-written hash, and only changed-or-new files are copied. Reusing
|
|
15
|
+
* mux's manifest is safe — entries are keyed by absolute dest path, so the
|
|
16
|
+
* watchtower dests (~/.claude-cabinet/watchtower/...) never collide with mux's
|
|
17
|
+
* (~/.config/mux/..., ~/.local/bin/...).
|
|
18
|
+
*
|
|
19
|
+
* REFRESH-ONLY semantics (critical): if the runtime directory does NOT already
|
|
20
|
+
* exist, this returns immediately with status 'absent' and writes nothing.
|
|
21
|
+
* Fresh runtime setup — launchd plist / cron, config.json, migrate-keys, the
|
|
22
|
+
* coherence assertion — is the `/watchtower install` SKILL.md step's job and is
|
|
23
|
+
* NOT replicated here. This function only keeps the code/docs/hooks of an
|
|
24
|
+
* EXISTING runtime current.
|
|
25
|
+
*
|
|
26
|
+
* The .mjs script set is globbed from templates/scripts/watchtower-*.mjs at
|
|
27
|
+
* load time (single source of truth — a newly shipped runtime script is picked
|
|
28
|
+
* up automatically; that "new file appears" case was the watchtower-advisories
|
|
29
|
+
* failure). The shell runners, session hooks, and cabinet docs are mapped
|
|
30
|
+
* explicitly since they live in different template subtrees and land in
|
|
31
|
+
* different runtime subdirs.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
const path = require('path');
|
|
36
|
+
const os = require('os');
|
|
37
|
+
const crypto = require('crypto');
|
|
38
|
+
|
|
39
|
+
const CC_HOME = path.join(os.homedir(), '.claude-cabinet');
|
|
40
|
+
const GLOBAL_MANIFEST_PATH = path.join(CC_HOME, 'global-manifest.json');
|
|
41
|
+
const RUNTIME_DIR = path.join(CC_HOME, 'watchtower');
|
|
42
|
+
const RUNTIME_SCRIPTS_DIR = path.join(RUNTIME_DIR, 'scripts');
|
|
43
|
+
const RUNTIME_HOOKS_DIR = path.join(RUNTIME_DIR, 'hooks');
|
|
44
|
+
const RUNTIME_CABINET_DIR = path.join(RUNTIME_DIR, 'cabinet');
|
|
45
|
+
const TEMPLATE_DIR = path.resolve(__dirname, '..', 'templates');
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build the MANAGED_FILES list: { src (absolute), dest (absolute), mode? }.
|
|
49
|
+
*
|
|
50
|
+
* - ALL templates/scripts/watchtower-*.mjs → runtime scripts/
|
|
51
|
+
* - the watchtower shell runners under scripts/ → runtime scripts/ (0o755)
|
|
52
|
+
* - templates/hooks/watchtower-session-*.sh → runtime hooks/ (0o755)
|
|
53
|
+
* - the two cabinet docs → runtime cabinet/
|
|
54
|
+
*
|
|
55
|
+
* Globbing the .mjs set keeps it the single source of truth — no hand-picked
|
|
56
|
+
* subset that can drift as new runtime scripts ship.
|
|
57
|
+
*/
|
|
58
|
+
function buildManagedFiles() {
|
|
59
|
+
const files = [];
|
|
60
|
+
const scriptsDir = path.join(TEMPLATE_DIR, 'scripts');
|
|
61
|
+
|
|
62
|
+
// All watchtower runtime .mjs scripts (globbed — complete set, no drift).
|
|
63
|
+
if (fs.existsSync(scriptsDir)) {
|
|
64
|
+
for (const name of fs.readdirSync(scriptsDir)) {
|
|
65
|
+
if (/^watchtower-.*\.mjs$/.test(name)) {
|
|
66
|
+
files.push({
|
|
67
|
+
src: path.join(scriptsDir, name),
|
|
68
|
+
dest: path.join(RUNTIME_SCRIPTS_DIR, name),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Shell runners the runtime executes (cron/launchd target these). They live
|
|
75
|
+
// in templates/scripts/ and land beside the .mjs in the runtime scripts/ dir.
|
|
76
|
+
const shellRunners = [
|
|
77
|
+
'watchtower-ring1-runner.sh',
|
|
78
|
+
'watchtower-ring2-runner.sh',
|
|
79
|
+
'watchtower-ring4-runner.sh',
|
|
80
|
+
'watchtower-status.sh',
|
|
81
|
+
];
|
|
82
|
+
for (const name of shellRunners) {
|
|
83
|
+
files.push({
|
|
84
|
+
src: path.join(scriptsDir, name),
|
|
85
|
+
dest: path.join(RUNTIME_SCRIPTS_DIR, name),
|
|
86
|
+
mode: 0o755,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Session hooks → runtime hooks/.
|
|
91
|
+
const hooksDir = path.join(TEMPLATE_DIR, 'hooks');
|
|
92
|
+
for (const name of ['watchtower-session-start.sh', 'watchtower-session-end.sh']) {
|
|
93
|
+
files.push({
|
|
94
|
+
src: path.join(hooksDir, name),
|
|
95
|
+
dest: path.join(RUNTIME_HOOKS_DIR, name),
|
|
96
|
+
mode: 0o755,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Cabinet docs the runtime / advisory pass reads → runtime cabinet/.
|
|
101
|
+
const cabinetDir = path.join(TEMPLATE_DIR, 'cabinet');
|
|
102
|
+
for (const name of ['advisories-state-schema.md', 'watchtower-contracts.md']) {
|
|
103
|
+
files.push({
|
|
104
|
+
src: path.join(cabinetDir, name),
|
|
105
|
+
dest: path.join(RUNTIME_CABINET_DIR, name),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return files;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const MANAGED_FILES = buildManagedFiles();
|
|
113
|
+
|
|
114
|
+
function sha256(content) {
|
|
115
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function readGlobalManifest() {
|
|
119
|
+
if (!fs.existsSync(GLOBAL_MANIFEST_PATH)) return { files: {} };
|
|
120
|
+
try {
|
|
121
|
+
const m = JSON.parse(fs.readFileSync(GLOBAL_MANIFEST_PATH, 'utf8'));
|
|
122
|
+
// Guard shape drift: manifest.files is indexed unconditionally below.
|
|
123
|
+
if (typeof m !== 'object' || m === null || Array.isArray(m)) return { files: {} };
|
|
124
|
+
if (typeof m.files !== 'object' || m.files === null || Array.isArray(m.files)) m.files = {};
|
|
125
|
+
return m;
|
|
126
|
+
} catch {
|
|
127
|
+
return { files: {} };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function writeGlobalManifest(manifest) {
|
|
132
|
+
fs.mkdirSync(path.dirname(GLOBAL_MANIFEST_PATH), { recursive: true });
|
|
133
|
+
const tmp = GLOBAL_MANIFEST_PATH + '.tmp';
|
|
134
|
+
fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2));
|
|
135
|
+
fs.renameSync(tmp, GLOBAL_MANIFEST_PATH);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Refresh the code/docs/hooks of an EXISTING watchtower runtime, content-aware.
|
|
140
|
+
*
|
|
141
|
+
* @param {Object} opts
|
|
142
|
+
* @param {boolean} [opts.dryRun]
|
|
143
|
+
* @param {string[]} [opts.results] — human-readable lines are pushed here; a
|
|
144
|
+
* fresh array is created and returned when not supplied.
|
|
145
|
+
* @returns {{ results: string[], status: 'absent'|'unchanged'|'refreshed' }}
|
|
146
|
+
*/
|
|
147
|
+
function refreshWatchtowerRuntime(opts = {}) {
|
|
148
|
+
const dryRun = !!opts.dryRun;
|
|
149
|
+
const results = opts.results || [];
|
|
150
|
+
|
|
151
|
+
// Refresh-only: never bootstrap a fresh runtime here. Fresh setup (launchd,
|
|
152
|
+
// cron, config.json, migrate-keys, coherence assertion) is the
|
|
153
|
+
// `/watchtower install` SKILL.md step's job. No runtime dir ⇒ no-op.
|
|
154
|
+
if (!fs.existsSync(RUNTIME_DIR)) {
|
|
155
|
+
return { results, status: 'absent' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
results.push('Refreshing watchtower runtime (content-aware)');
|
|
159
|
+
|
|
160
|
+
const manifest = readGlobalManifest();
|
|
161
|
+
let copiedCount = 0;
|
|
162
|
+
|
|
163
|
+
for (const file of MANAGED_FILES) {
|
|
164
|
+
if (!fs.existsSync(file.src)) {
|
|
165
|
+
results.push(` ⚠ Template missing: ${file.src}`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const content = fs.readFileSync(file.src);
|
|
170
|
+
const hash = sha256(content);
|
|
171
|
+
|
|
172
|
+
// Content-aware skip: compare the template hash to the runtime file's
|
|
173
|
+
// ACTUAL on-disk bytes, NOT the cached manifest. The manifest can lie — a
|
|
174
|
+
// hand-edited or externally-replaced runtime file still carries the hash
|
|
175
|
+
// the manifest recorded at last write, so a manifest-only check would skip
|
|
176
|
+
// it and leave the divergence running. Reading the real file makes the
|
|
177
|
+
// refresh self-healing: any runtime file that differs from the template
|
|
178
|
+
// (or is missing) is rewritten, so a reinstall reliably reconverges the
|
|
179
|
+
// runtime onto the template instead of silently leaving it stale.
|
|
180
|
+
let runtimeHash = null;
|
|
181
|
+
if (fs.existsSync(file.dest)) {
|
|
182
|
+
try {
|
|
183
|
+
runtimeHash = sha256(fs.readFileSync(file.dest));
|
|
184
|
+
} catch {
|
|
185
|
+
runtimeHash = null; // unreadable runtime file → treat as divergent
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (runtimeHash === hash) {
|
|
190
|
+
// Already in sync on disk. Keep the manifest honest as bookkeeping.
|
|
191
|
+
if (!dryRun) manifest.files[file.dest] = hash;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (dryRun) {
|
|
196
|
+
results.push(` [dry-run] ${path.relative(TEMPLATE_DIR, file.src)} → ${file.dest}`);
|
|
197
|
+
} else {
|
|
198
|
+
fs.mkdirSync(path.dirname(file.dest), { recursive: true });
|
|
199
|
+
fs.writeFileSync(file.dest, content);
|
|
200
|
+
if (file.mode) {
|
|
201
|
+
fs.chmodSync(file.dest, file.mode);
|
|
202
|
+
}
|
|
203
|
+
manifest.files[file.dest] = hash;
|
|
204
|
+
}
|
|
205
|
+
copiedCount++;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!dryRun && copiedCount > 0) {
|
|
209
|
+
manifest.installedAt = new Date().toISOString();
|
|
210
|
+
writeGlobalManifest(manifest);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (copiedCount > 0) {
|
|
214
|
+
results.push(` ${copiedCount} runtime file${copiedCount !== 1 ? 's' : ''} refreshed under ${RUNTIME_DIR}`);
|
|
215
|
+
return { results, status: 'refreshed' };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
results.push(' watchtower runtime already current');
|
|
219
|
+
return { results, status: 'unchanged' };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// MANAGED_FILES, RUNTIME_DIR, and TEMPLATE_DIR are exported for the
|
|
223
|
+
// runtime-refresh test so it can seed a faithful, complete global manifest
|
|
224
|
+
// (single source of truth — the test never re-derives the file list).
|
|
225
|
+
module.exports = {
|
|
226
|
+
refreshWatchtowerRuntime,
|
|
227
|
+
MANAGED_FILES,
|
|
228
|
+
RUNTIME_DIR,
|
|
229
|
+
TEMPLATE_DIR,
|
|
230
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-claude-cabinet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "Claude Cabinet — opinionated process scaffolding for Claude Code projects",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-claude-cabinet": "bin/create-claude-cabinet.js"
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
],
|
|
23
23
|
"author": "Oren Magid",
|
|
24
24
|
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/orenmagid/claude-cabinet.git"
|
|
28
|
+
},
|
|
25
29
|
"engines": {
|
|
26
30
|
"node": ">=18"
|
|
27
31
|
},
|
|
@@ -27,7 +27,10 @@ briefing:
|
|
|
27
27
|
# Common: .claude/cabinet/_briefing-architecture.md, _briefing-jurisdictions.md, _briefing-api.md
|
|
28
28
|
standing-mandate: audit
|
|
29
29
|
# Add plan, execute, orient, debrief if this member should activate
|
|
30
|
-
# in those contexts. Most members are audit-only.
|
|
30
|
+
# in those contexts. Most members are audit-only. Two session-boundary
|
|
31
|
+
# contexts exist for standing advisors (watchtower installs):
|
|
32
|
+
# session-close (Ring 3's automatic transcript-fed advisor pass) and
|
|
33
|
+
# briefing (/briefing's live advisor panel).
|
|
31
34
|
tools:
|
|
32
35
|
# List every external tool/command this member uses.
|
|
33
36
|
# Format: "tool-name (scope -- what it does)"
|
|
@@ -37,8 +40,10 @@ tools:
|
|
|
37
40
|
# - grep patterns (all projects -- dead code detection)
|
|
38
41
|
# Use "tools: []" for pure-reasoning members with no tool stage.
|
|
39
42
|
directives:
|
|
40
|
-
# Only if standing-mandate includes plan, execute, orient,
|
|
41
|
-
# Each directive is a one-sentence focused
|
|
43
|
+
# Only if standing-mandate includes plan, execute, orient, debrief,
|
|
44
|
+
# session-close, or briefing. Each directive is a one-sentence focused
|
|
45
|
+
# task for that context. A mandate without a matching directive is a
|
|
46
|
+
# data error — consumers skip the member and say so.
|
|
42
47
|
# plan: >
|
|
43
48
|
# What to evaluate when reviewing a plan.
|
|
44
49
|
# execute: >
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
# Advisory dismissal state — schema and rules
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The watchtower **SessionStart context builder** surfaces stack-aware advisories (install the Ruby language server, register the Railway MCP, install `hookify`, …) via `scripts/watchtower-advisories.mjs` (act:f9ea075d). Without memory, every advisory re-nags every session — the same attention-fatigue pattern the watchtower rings were built to eliminate. This file defines the per-project state that gives advisories a memory, and the exact rules the advisory pass follows so an advisory is never *permanently* silenced by accident.
|
|
4
|
+
|
|
5
|
+
> **Actor.** The owner of these rules is `watchtower-advisories.mjs` (`runAdvisoryPass`), called by the context builder when watchtower is installed. On a project WITHOUT watchtower, orient runs a thin one-shot fallback (it may shell the same module via `node scripts/watchtower-advisories.mjs` when present, else surface basic install hints with no persistent dismissal state). The module is the single implementation of the rules below; do not re-encode them anywhere else.
|
|
4
6
|
|
|
5
7
|
## Where it lives
|
|
6
8
|
|
|
7
|
-
`.claude/cabinet/advisories-state.json` — **per project, generated at runtime**, NOT shipped as a template.
|
|
9
|
+
`.claude/cabinet/advisories-state.json` — **per project, generated at runtime**, NOT shipped as a template. The advisory pass creates it on first write. It must never be added to a module's template array: a shipped stub would overwrite a project's real dismissal history on reinstall (the `.ccrc.json` clobber class of bug). If the file is absent OR malformed JSON, every advisory is treated as never-seen (the reader degrades to `{}`, never throws).
|
|
10
|
+
|
|
11
|
+
> Worktree note: `.claude/cabinet/` is copied per worktree, so dismissal state can diverge between a worktree and its main checkout. That is acceptable — advisories are advisory — and is the reason this is project-local, not user-global. **Path-consistency rule:** the pass reads AND writes the state at the SAME path (the session's `--project-path` cwd). Never read from one path and write to another, or a decline recorded in one place is invisible from the other and the advisory re-nags forever.
|
|
12
|
+
|
|
13
|
+
## Reserved `_meta` key
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
`_meta` is a **reserved top-level key**, NOT an advisory entry. It holds pass-level bookkeeping — currently `{ "last_probe": "<UTC YYYY-MM-DD>" }`, the throttle stamp for the `claude plugin list` install-probe (run at most once/day/checkout; UTC to match `last_shown` and sqlite `date('now')`). Any consumer iterating advisory entries (e.g. `Object.entries(state)`) MUST skip `_meta`. No advisory id may be named `_meta`.
|
|
10
16
|
|
|
11
17
|
## Schema
|
|
12
18
|
|
|
@@ -30,9 +36,26 @@ Orient surfaces stack-aware advisories (install the Ruby language server, regist
|
|
|
30
36
|
- **`last_shown`** — ISO date of the most recent surfacing.
|
|
31
37
|
- **`signal`** — *the key field that makes "resurface if the stack changed" actually work.* A short, deterministic fingerprint of the stack indicators present when the advisory was last shown/declined. For a multi-indicator advisory like Ruby (`Gemfile` OR `*.rb`), the fingerprint records *which* indicators were present (e.g. `gemfile` vs `gemfile+rb`), so a later change is detectable. Without this stored snapshot, orient has only the *current* indicators and no baseline to diff against — which is the gap this schema closes.
|
|
32
38
|
|
|
33
|
-
##
|
|
39
|
+
## Computing the signal (the indicator fingerprint)
|
|
40
|
+
|
|
41
|
+
A signal is a deterministic fingerprint of the stack indicators present now. Two rules keep it stable:
|
|
42
|
+
|
|
43
|
+
- **Sorted tokens.** A multi-indicator advisory joins its present indicators (e.g. Ruby = `gemfile`, `rb`, or `gemfile+rb`) **after sorting** them. Readdir order is not stable; without the sort, `gemfile+rb` and `rb+gemfile` would alternate and spuriously re-arm a declined advisory every session.
|
|
44
|
+
- **Bounded stack scan.** Source-file indicators (`*.ts`, `*.py`, `*.rb`) are detected by a **depth-limited (≤3) walk** with a denylist (`node_modules`, `dist`, `build`, `vendor`, `coverage`, `target`, `out`, plus all dot-dirs) and **early-exit on first match** (existence only). A recursive walk would be pathological on the session-start critical path; a root-only scan would silently miss the common `src/**/*.ts` layout (a TS project with no root `tsconfig.json`).
|
|
45
|
+
|
|
46
|
+
## The probe is tri-state (the fourth input state)
|
|
34
47
|
|
|
35
|
-
|
|
48
|
+
The install-probe (`claude plugin list`) answers "is this plugin installed?" — but it can fail to answer (claude not on PATH, nonzero exit, timeout). Its result is therefore **`true | false | null`**, never a boolean. `null` ("unknown") is a distinct input state and is handled per advisory **kind**:
|
|
49
|
+
|
|
50
|
+
- **probe-suppressed** (the LSP advisories): the signal is on disk (stack files); the probe only *suppresses* (confirms installed → terminal). `installed===null` → **still surface** from the signal/state rules; freeze only the `installed` transition.
|
|
51
|
+
- **probe-gated** (`plugin:hookify`): the surfacing predicate *is* the probe ("hookify not installed"). `installed===null` → predicate unknowable → **suppress this session, do NOT increment count, do NOT write state** (freeze the entry untouched).
|
|
52
|
+
- **no-probe** (`mcp:railway`, `briefing-file`, `registry-orphan`): pure filesystem/config signal; the probe is irrelevant.
|
|
53
|
+
|
|
54
|
+
In all kinds, `installed===true` flips the entry to terminal `installed`.
|
|
55
|
+
|
|
56
|
+
## The rules the advisory pass follows
|
|
57
|
+
|
|
58
|
+
Before surfacing any advisory, the pass computes the advisory's **current signal** (fingerprint of the indicators present now) and reads the stored entry:
|
|
36
59
|
|
|
37
60
|
1. **No entry / file absent** → surface it. Write `{status:"suggested", count:1, last_shown:today, signal:current}`.
|
|
38
61
|
2. **`installed`** → never surface (terminal). (Re-probe may flip a `suggested`/`declined` entry to `installed`; never the reverse automatically.)
|
|
@@ -64,5 +87,9 @@ Document any new advisory's signal source here when you add it, and call out exp
|
|
|
64
87
|
| `lsp:rust` | `Cargo.toml` | `/plugin install rust-analyzer-lsp` |
|
|
65
88
|
| `lsp:go` | `go.mod` | `/plugin install gopls-lsp` |
|
|
66
89
|
| `lsp:ruby` | `Gemfile` or `*.rb` | `/plugin install ruby-lsp@claude-plugins-official` (also needs `gem install ruby-lsp` AND `ENABLE_LSP_TOOL=1`) |
|
|
67
|
-
| `mcp:railway` | `railway.toml` and no railway key in `~/.claude.json` | local: `railway setup agent -y` · remote: register `mcp.railway.com` (OAuth) |
|
|
68
|
-
| `plugin:hookify` | `.claude/rules/enforcement-pipeline.md` exists and hookify not in `claude plugin list` (signal is **static
|
|
90
|
+
| `mcp:railway` | `railway.toml` and no railway key in `~/.claude.json` (no-probe) | local: `railway setup agent -y` · remote: register `mcp.railway.com` (OAuth) |
|
|
91
|
+
| `plugin:hookify` | `.claude/rules/enforcement-pipeline.md` exists and hookify not in `claude plugin list` (signal is **static**; probe-gated) | `/plugin install hookify` |
|
|
92
|
+
| `briefing-file` | `.claude/cabinet/_briefing.md` is **absent** (canonical location — where cabinet members read; `.claude/briefing/` holds only installer-seeded templates) (signal `missing`, **static**; no-probe) | run `/onboard` to create one |
|
|
93
|
+
| `registry-orphan` | `~/.claude/cc-registry.json` lists project path(s) that no longer exist (signal = sorted orphan-name set, so it re-arms when the registry changes; no-probe) | remove the dead entr(y/ies) from `~/.claude/cc-registry.json` |
|
|
94
|
+
|
|
95
|
+
> Note: `mcp:railway` keys on `railway.toml`, which Ring 1 also marker-checks for deploy detection — these are independent reads of the same file for different purposes; do not consolidate them (the advisory adds the `~/.claude.json` registration predicate Ring 1 lacks).
|
|
@@ -19,9 +19,23 @@ state.
|
|
|
19
19
|
| Writer | When | What |
|
|
20
20
|
|--------|------|------|
|
|
21
21
|
| `/execute` `post-impl-checklist` phase | every run past its no-op guard | increments `runs`; per triggered dimension increments `fires`, sets `last_fired` |
|
|
22
|
-
|
|
|
22
|
+
| Ring 3 close `checklist-catch` lens (Phase 2p) | session close, when the transcript shows a surfaced check that caught a real bug | appends to that dimension's `catches` |
|
|
23
|
+
| `/debrief` `checklist-feedback` phase (pre-watchtower; retiring) | when a session bug WAS caught via a surfaced check | appends to that dimension's `catches` |
|
|
23
24
|
| `/audit` `checklist-pruning` phase | every pruning verdict (including "keep") | appends to `pruning_reviews` |
|
|
24
25
|
|
|
26
|
+
The **catch side is recorded automatically at session close** by the
|
|
27
|
+
Ring 3 `checklist-catch` lens (`watchtower-ring3-close.mjs`, Phase 2p)
|
|
28
|
+
for watchtower-installed projects — it reads the transcript, attributes
|
|
29
|
+
catches to a named dimension, and writes them via the protocol below.
|
|
30
|
+
`/debrief`'s `checklist-feedback` phase did this interactively and is
|
|
31
|
+
retired by the watchtower burn-in (its sharpening side — proposing new
|
|
32
|
+
checks for bugs that *slipped through* — needs operator approval and
|
|
33
|
+
cannot move to a background ring; only the catch tally moved). The two
|
|
34
|
+
writers are mutually exclusive in practice: a watchtower project closes
|
|
35
|
+
through Ring 3, a non-watchtower project through debrief, and both
|
|
36
|
+
follow the same atomic, fail-open write protocol — so the catches a
|
|
37
|
+
later audit prunes against are recorded the same way regardless.
|
|
38
|
+
|
|
25
39
|
## Schema (`schema_version: 1`)
|
|
26
40
|
|
|
27
41
|
```json
|
|
@@ -107,9 +107,10 @@ to do its own work. One cabinet member consults another mid-evaluation.
|
|
|
107
107
|
needs full conversation history) references another cabinet member's known
|
|
108
108
|
findings — from memory, from audit history, or from prior session output.
|
|
109
109
|
|
|
110
|
-
**Example:**
|
|
111
|
-
"Has this kind of change been done before? What
|
|
112
|
-
lessons from prior sessions relevant to what was
|
|
110
|
+
**Example:** At session close (Ring 3's advisor pass), the historian is
|
|
111
|
+
activated to check: "Has this kind of change been done before? What
|
|
112
|
+
happened? Are there lessons from prior sessions relevant to what was
|
|
113
|
+
just completed?"
|
|
113
114
|
|
|
114
115
|
**Example:** During planning, the organized-mind cabinet member might need
|
|
115
116
|
the historian's input: "Has this kind of information architecture been
|