create-claude-cabinet 0.46.0 → 0.48.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 +3 -6
- package/lib/CLAUDE.md +218 -0
- package/lib/cli.js +397 -382
- package/lib/copy.js +182 -6
- package/lib/db-setup.js +122 -17
- package/lib/engagement-server-setup.js +1 -17
- package/lib/engagement-setup.js +1 -1
- package/lib/installer-gate.js +135 -0
- package/lib/metadata.js +51 -2
- package/lib/modules.js +292 -0
- package/lib/mux-setup.js +1 -17
- package/lib/project-context.js +16 -96
- package/lib/settings-merge.js +194 -13
- package/lib/site-audit-setup.js +23 -7
- package/lib/verify-setup.js +20 -9
- package/lib/watchtower-setup.js +23 -7
- package/package.json +1 -1
- package/templates/CLAUDE.md +959 -0
- package/templates/briefing/_briefing-template.md +6 -11
- package/templates/cabinet/advisories-state-schema.md +1 -1
- package/templates/cabinet/checklist-stats-schema.md +15 -1
- package/templates/cabinet/eval-protocol.md +27 -10
- package/templates/cabinet/memory-lifecycle-contract.md +135 -0
- package/templates/cabinet/pib-db-access.md +13 -0
- package/templates/cabinet/watchtower-contracts.md +453 -0
- package/templates/cabinet/worktree-invocation-contract.md +111 -0
- package/templates/engagement/OVERVIEW.md +12 -0
- package/templates/engagement/__tests__/checklist-visibility.test.mjs +76 -0
- package/templates/engagement/__tests__/engagement.test.mjs +177 -2
- package/templates/engagement/__tests__/invoice-doc.test.mjs +195 -0
- package/templates/engagement/engagement-checklist.mjs +26 -2
- package/templates/engagement/engagement-preview.mjs +100 -0
- package/templates/engagement/engagement-schema.md +248 -6
- package/templates/engagement/engagement.mjs +391 -11
- package/templates/engagement/pib-db-patches/pib-db-lib.mjs +49 -9
- package/templates/engagement/pib-db-patches/pib-db-mcp-server.mjs +5 -44
- package/templates/engagement/pib-db-patches/pib-db-schema.sql +1 -1
- package/templates/engagement/pib-db-patches/pib-db.mjs +9 -3
- package/templates/engagement/sql-constants.mjs +9 -1
- package/templates/engagement-server/__tests__/cross-tenant-auth.test.mjs +87 -0
- package/templates/engagement-server/server.mjs +65 -40
- package/templates/hooks/action-completion-gate.sh +19 -3
- package/templates/hooks/action-quality-gate.sh +10 -0
- package/templates/hooks/memory-index-guard.sh +17 -11
- package/templates/hooks/skill-telemetry.sh +10 -5
- package/templates/hooks/skill-tool-telemetry.sh +11 -5
- package/templates/hooks/watchtower-session-start.sh +24 -2
- package/templates/mcp/pib-db.json +1 -4
- package/templates/mux/bin/mux +8 -1
- package/templates/mux/config/help.txt +1 -0
- package/templates/rules/acknowledge-when-corrected.md +33 -0
- package/templates/rules/enforcement-pipeline.md +0 -12
- 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__/ahead-check-origin.test.mjs +355 -0
- package/templates/scripts/__tests__/api-usage-idle-gate.test.mjs +81 -0
- package/templates/scripts/__tests__/batch-disposition.test.mjs +98 -0
- package/templates/scripts/__tests__/bsql-loader-verify.test.mjs +47 -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__/hook-runner.test.mjs +247 -0
- package/templates/scripts/__tests__/inbox-assessment.test.mjs +341 -0
- package/templates/scripts/__tests__/memory-reachability.test.mjs +150 -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-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 +120 -0
- 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-pib-error-surfacing.test.mjs +69 -0
- package/templates/scripts/__tests__/ring1-script-drift.test.mjs +126 -0
- package/templates/scripts/__tests__/ring1-stale-open-counts.test.mjs +143 -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__/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 +92 -12
- 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__/ring3-thread-capture.test.mjs +213 -0
- package/templates/scripts/__tests__/ring4-reconcile.test.mjs +393 -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-status-ring4.test.mjs +89 -0
- package/templates/scripts/__tests__/watchtower-sync.test.mjs +311 -0
- package/templates/scripts/audit-coherence-check.mjs +123 -0
- package/templates/scripts/audit-synth.mjs +117 -0
- package/templates/scripts/load-triage-history.js +5 -2
- package/templates/scripts/merge-findings.js +4 -1
- package/templates/scripts/patterns-scope-check.mjs +122 -0
- package/templates/scripts/pib-db-lib.mjs +61 -11
- package/templates/scripts/pib-db-mcp-server.mjs +7 -45
- package/templates/scripts/pib-db-path.mjs +61 -0
- package/templates/scripts/pib-db.mjs +7 -3
- package/templates/scripts/review-server.mjs +7 -12
- package/templates/scripts/triage-server.mjs +7 -8
- package/templates/scripts/validate-memory.mjs +214 -16
- package/templates/scripts/watchtower-advisories.mjs +7 -3
- package/templates/scripts/watchtower-build-context.mjs +192 -8
- package/templates/scripts/watchtower-cross-ring-reader.mjs +714 -0
- package/templates/scripts/watchtower-hook-runner.mjs +422 -0
- package/templates/scripts/watchtower-inbox-assessment.mjs +706 -0
- package/templates/scripts/watchtower-lib.mjs +555 -8
- package/templates/scripts/watchtower-narrative-corpus.mjs +385 -0
- package/templates/scripts/watchtower-phase-shim.mjs +171 -0
- package/templates/scripts/watchtower-queue.mjs +341 -3
- package/templates/scripts/watchtower-ring1.mjs +481 -81
- package/templates/scripts/watchtower-ring2.mjs +748 -80
- package/templates/scripts/watchtower-ring3-close.mjs +1147 -167
- package/templates/scripts/watchtower-ring4-runner.sh +85 -0
- package/templates/scripts/watchtower-ring4.mjs +753 -0
- package/templates/scripts/watchtower-routines.mjs +1 -1
- package/templates/scripts/watchtower-snapshot.mjs +452 -0
- package/templates/scripts/watchtower-status.sh +17 -0
- package/templates/scripts/watchtower-sync.mjs +393 -0
- package/templates/scripts/work-tracker-server.mjs +26 -22
- package/templates/skills/audit/SKILL.md +18 -5
- package/templates/skills/audit/phases/structural-checks.md +22 -0
- package/templates/skills/briefing/SKILL.md +289 -41
- package/templates/skills/cabinet-accessibility/SKILL.md +1 -1
- package/templates/skills/cabinet-process-therapist/SKILL.md +28 -0
- package/templates/skills/cabinet-security/SKILL.md +11 -0
- package/templates/skills/catch-up/SKILL.md +113 -0
- package/templates/skills/cc-publish/SKILL.md +89 -18
- 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 +21 -4
- package/templates/skills/debrief/phases/audit-pattern-capture.md +13 -0
- 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 +71 -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 +177 -31
- package/templates/skills/memory/SKILL.md +22 -6
- package/templates/skills/orient/SKILL.md +75 -5
- 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/pulse/SKILL.md +10 -8
- package/templates/skills/qa-drain/SKILL.md +38 -0
- package/templates/skills/qa-handoff/SKILL.md +287 -46
- package/templates/skills/setup-accounts/SKILL.md +38 -16
- package/templates/skills/spring-clean/SKILL.md +1 -1
- package/templates/skills/spring-clean/phases/execute-decisions.md +1 -1
- package/templates/skills/threads/SKILL.md +4 -0
- package/templates/skills/validate/phases/validators.md +127 -0
- package/templates/skills/watchtower/SKILL.md +174 -30
- package/templates/verify-runtime/CONVENTIONS.md +9 -0
- package/templates/verify-runtime/README.md +37 -0
- package/templates/watchtower/config.json.template +8 -2
- package/templates/workflows/deliberative-audit.js +258 -87
- package/templates/workflows/execute-group-complete.js +26 -3
- package/templates/workflows/execute-group-implement.js +16 -2
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// All writes use atomic temp+rename per watchtower-contracts.md.
|
|
5
5
|
// Queue uses directory listing, not index files (no-index convention).
|
|
6
6
|
|
|
7
|
-
import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync, renameSync, unlinkSync } from 'fs';
|
|
7
|
+
import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync, renameSync, unlinkSync, realpathSync } from 'fs';
|
|
8
8
|
import { join } from 'path';
|
|
9
9
|
import { randomBytes } from 'crypto';
|
|
10
|
+
import { pathToFileURL } from 'url';
|
|
10
11
|
|
|
11
12
|
const WATCHTOWER_DIR = process.env.WATCHTOWER_DIR
|
|
12
13
|
|| join(process.env.HOME, '.claude-cabinet', 'watchtower');
|
|
@@ -93,6 +94,15 @@ const URGENCY_ORDER = { urgent: 0, normal: 1, low: 2 };
|
|
|
93
94
|
// sin, a silent stamp with no coverage look, is impossible rather than
|
|
94
95
|
// discouraged. Keep all qa-handoff domain knowledge fenced here; the CRUD
|
|
95
96
|
// functions below only call into it.
|
|
97
|
+
//
|
|
98
|
+
// merge_state (evidence.merge_state): a handoff is normally filed AFTER the
|
|
99
|
+
// worktree branch merged ('merged' — the original and default contract). A
|
|
100
|
+
// handoff filed BEFORE the merge (the operator is gating the merge on an
|
|
101
|
+
// in-flight staging deploy) carries 'merge-pending': the skill defers stage-2
|
|
102
|
+
// dispatch and the pickup prompt says "merge then QA" instead of asserting a
|
|
103
|
+
// merge — no more hand-flagging merged_into: "PENDING". The recipient gate is
|
|
104
|
+
// unchanged: a merge-pending handoff still resolves only through a stamped
|
|
105
|
+
// qa_verdict, cited against the post-merge commit once the merge has happened.
|
|
96
106
|
|
|
97
107
|
const QA_CATEGORY = 'qa-handoff';
|
|
98
108
|
|
|
@@ -129,6 +139,28 @@ export function normalizeQaVerdictToken(raw) {
|
|
|
129
139
|
return null;
|
|
130
140
|
}
|
|
131
141
|
|
|
142
|
+
// The two legal merge states for a qa-handoff item.
|
|
143
|
+
export const QA_MERGE_STATES = ['merged', 'merge-pending'];
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Normalize a qa-handoff merge_state token to canonical form.
|
|
147
|
+
* Absent (null/undefined) defaults to 'merged' — the original contract, where
|
|
148
|
+
* a handoff was always filed AFTER the worktree branch merged. A handoff filed
|
|
149
|
+
* before the merge carries 'merge-pending'. Tolerates separator/case drift
|
|
150
|
+
* (merge_pending, "merge pending", pending, unmerged); returns null only for a
|
|
151
|
+
* present-but-illegal token, so a typo cannot silently read as 'merged'.
|
|
152
|
+
* @param {*} raw
|
|
153
|
+
* @returns {'merged'|'merge-pending'|null}
|
|
154
|
+
*/
|
|
155
|
+
export function normalizeMergeState(raw) {
|
|
156
|
+
if (raw == null) return 'merged';
|
|
157
|
+
if (typeof raw !== 'string') return null;
|
|
158
|
+
const t = raw.normalize('NFKC').trim().toLowerCase().replace(/[\s_]+/g, '-');
|
|
159
|
+
if (t === 'merged') return 'merged';
|
|
160
|
+
if (['merge-pending', 'mergepending', 'pending', 'unmerged'].includes(t)) return 'merge-pending';
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
132
164
|
function gateError(item, message) {
|
|
133
165
|
// Never echo resolution/notes content back — name fields only.
|
|
134
166
|
return new Error(`qa-handoff gate: cannot close ${item.id} — ${message}`);
|
|
@@ -285,6 +317,82 @@ export function emitPatternPromotion({
|
|
|
285
317
|
});
|
|
286
318
|
}
|
|
287
319
|
|
|
320
|
+
// --- High-confidence sign-off predicate (bulk-triage batching) ---
|
|
321
|
+
//
|
|
322
|
+
// Most pending items are knowledge-extraction items the rings already DRAFTED
|
|
323
|
+
// (Ring 3 mints a memory `draft_artifact` from the session transcript). These
|
|
324
|
+
// aren't judgment calls — they're SIGN-OFFS on work the system already did.
|
|
325
|
+
// Because each is uniquely titled, title-grouping degrades bulk triage to a
|
|
326
|
+
// per-item walk, burning the same decision fuel on a sign-off as on a hard
|
|
327
|
+
// choice. The predicate below isolates that class so bulk triage can offer it
|
|
328
|
+
// as ONE explicit batch sign-off (still human-confirmed — never auto-resolved;
|
|
329
|
+
// the frozen-at-propose invariant holds because the operator approves the
|
|
330
|
+
// whole batch before any applyBatch write).
|
|
331
|
+
//
|
|
332
|
+
// The signal reuses the SAME "drafts ready" cue the SessionStart context
|
|
333
|
+
// builder already counts (watchtower-build-context.mjs: knowledge-extraction +
|
|
334
|
+
// non-empty draft_artifact) — single source for "the system pre-authored this".
|
|
335
|
+
// An item is held back for individual attention (NOT signed off in a batch)
|
|
336
|
+
// when it is urgent, low-confidence, or carries no pre-drafted artifact —
|
|
337
|
+
// those are the ones that actually want a human look. Gated categories
|
|
338
|
+
// (qa-handoff) are never high-confidence sign-off by construction: they exit
|
|
339
|
+
// only through their own recipient gate.
|
|
340
|
+
|
|
341
|
+
// Categories whose pre-drafted, normal-priority items are sign-off-shaped.
|
|
342
|
+
// Today only knowledge-extraction (Ring 3's memory drafts). A future
|
|
343
|
+
// pre-drafted category joins here and inherits batch sign-off for free.
|
|
344
|
+
const SIGNOFF_CATEGORIES = new Set(['knowledge-extraction']);
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* True when an item is a high-confidence SIGN-OFF — pre-drafted work the
|
|
348
|
+
* system already did, safe to approve as part of an explicit one-shot batch
|
|
349
|
+
* rather than as its own micro-decision. False for anything that wants an
|
|
350
|
+
* individual human look (urgent, low-confidence, or no pre-drafted artifact)
|
|
351
|
+
* and for any gated-category item (those leave only through their own gate).
|
|
352
|
+
*
|
|
353
|
+
* Conservative by design: when in doubt the item is surfaced individually.
|
|
354
|
+
* The operator still confirms the whole batch before any write — this only
|
|
355
|
+
* decides WHICH items are eligible to ride in a batch sign-off.
|
|
356
|
+
* @param {object} item
|
|
357
|
+
* @returns {boolean}
|
|
358
|
+
*/
|
|
359
|
+
export function isHighConfidenceSignoff(item) {
|
|
360
|
+
if (!item || typeof item !== 'object') return false;
|
|
361
|
+
if (item.status && item.status !== 'pending') return false;
|
|
362
|
+
if (GATED_CATEGORIES.has(item.category)) return false;
|
|
363
|
+
if (!SIGNOFF_CATEGORIES.has(item.category)) return false;
|
|
364
|
+
// The system must have actually drafted something — a non-empty artifact is
|
|
365
|
+
// the proof the work is done and only needs a sign-off.
|
|
366
|
+
if (typeof item.draft_artifact !== 'string' || !item.draft_artifact.trim()) return false;
|
|
367
|
+
// Urgent items want eyes, not a batch nod.
|
|
368
|
+
if (item.urgency === 'urgent') return false;
|
|
369
|
+
// An explicit low-confidence stamp means "look at this one".
|
|
370
|
+
if (item.confidence === 'low') return false;
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Partition pending items into the high-confidence sign-off set (eligible for
|
|
376
|
+
* a one-shot batch approval) and the individual set (everything that wants a
|
|
377
|
+
* human look — unusual / low-confidence items AND every gated item). Gated
|
|
378
|
+
* items are ALWAYS individual: the predicate excludes them, so they can never
|
|
379
|
+
* leak into the batch, and applyBatch would reject them anyway.
|
|
380
|
+
*
|
|
381
|
+
* Pure read helper — no writes, no I/O. Bulk triage uses it to decide what to
|
|
382
|
+
* offer as a batch vs. one by one; the operator still approves the batch.
|
|
383
|
+
* @param {Array} items - pending inbox items (e.g. from listPending())
|
|
384
|
+
* @returns {{signoff: Array, individual: Array}}
|
|
385
|
+
*/
|
|
386
|
+
export function partitionForBatchSignoff(items) {
|
|
387
|
+
const signoff = [];
|
|
388
|
+
const individual = [];
|
|
389
|
+
for (const item of Array.isArray(items) ? items : []) {
|
|
390
|
+
if (isHighConfidenceSignoff(item)) signoff.push(item);
|
|
391
|
+
else individual.push(item);
|
|
392
|
+
}
|
|
393
|
+
return { signoff, individual };
|
|
394
|
+
}
|
|
395
|
+
|
|
288
396
|
// --- Exports ---
|
|
289
397
|
|
|
290
398
|
/**
|
|
@@ -312,6 +420,38 @@ export function createItem({
|
|
|
312
420
|
desk = null,
|
|
313
421
|
}) {
|
|
314
422
|
ensureDir(QUEUE_DIR);
|
|
423
|
+
// qa-handoff merge_state is structural: a present-but-illegal token is
|
|
424
|
+
// rejected at the boundary (it can't silently read as 'merged'), and a legal
|
|
425
|
+
// one is canonicalized. Absent stays absent — readers default to 'merged'
|
|
426
|
+
// via normalizeMergeState (additive-field discipline, like desk/project_
|
|
427
|
+
// unresolved). Only qa-handoff items carry it; other categories are untouched.
|
|
428
|
+
if (category === QA_CATEGORY && evidence && evidence.merge_state !== undefined) {
|
|
429
|
+
const ms = normalizeMergeState(evidence.merge_state);
|
|
430
|
+
if (!ms) {
|
|
431
|
+
throw new Error(`createItem: illegal qa-handoff merge_state ${JSON.stringify(evidence.merge_state)} — must be one of: ${QA_MERGE_STATES.join(' | ')}`);
|
|
432
|
+
}
|
|
433
|
+
evidence = { ...evidence, merge_state: ms };
|
|
434
|
+
}
|
|
435
|
+
// A 'merged' handoff must reference work actually on main. merge_state
|
|
436
|
+
// replaced the old hand-flagged `merged_into: "PENDING — not yet merged"`
|
|
437
|
+
// antipattern, but a producer can still free-text a not-yet-merged marker
|
|
438
|
+
// into merged_into/merged_commit while leaving merge_state at its 'merged'
|
|
439
|
+
// default — handing off UNMERGED work the drain is then told to merge
|
|
440
|
+
// (field incident 2026-06-17). Reject it at the boundary: a pending marker
|
|
441
|
+
// means the merge has not happened, so the honest state is
|
|
442
|
+
// merge_state:'merge-pending' (which defers stage-2 dispatch), never a
|
|
443
|
+
// 'merged' handoff. Pure string check; the drain's Step 0 does the
|
|
444
|
+
// git-ancestry verification of merged_commit. (act:e859b3a3)
|
|
445
|
+
if (category === QA_CATEGORY && evidence
|
|
446
|
+
&& normalizeMergeState(evidence.merge_state) === 'merged') {
|
|
447
|
+
const NOT_YET_MERGED = /\b(pending|unmerged|not[\s-]*yet[\s-]*merged|to[\s-]*be[\s-]*merged|will[\s-]*merge)\b/i;
|
|
448
|
+
for (const k of ['merged_into', 'merged_commit']) {
|
|
449
|
+
const v = evidence[k];
|
|
450
|
+
if (typeof v === 'string' && NOT_YET_MERGED.test(v)) {
|
|
451
|
+
throw new Error(`createItem: qa-handoff filed as 'merged' but evidence.${k} flags the merge as not yet done (${JSON.stringify(v.slice(0, 60))}) — merge the branch first, or file with merge_state:'merge-pending' (which defers stage-2 dispatch). A 'merged' handoff must reference work actually on main.`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
315
455
|
const id = generateId();
|
|
316
456
|
const item = {
|
|
317
457
|
schema_version: 1,
|
|
@@ -532,8 +672,9 @@ export function listPending({ project, category, urgency, maxAge } = {}) {
|
|
|
532
672
|
|
|
533
673
|
/**
|
|
534
674
|
* List inbox items across ALL statuses with optional filters — the single
|
|
535
|
-
* source for "read non-pending items".
|
|
536
|
-
* fork
|
|
675
|
+
* source for "read non-pending items". Ring 2's private readAllQueueItems
|
|
676
|
+
* fork (which predated this helper) was deleted and now reads through this
|
|
677
|
+
* helper (act:3975348f).
|
|
537
678
|
* @param {object} filters
|
|
538
679
|
* @param {string} [filters.project] - exact match on item.project
|
|
539
680
|
* @param {string} [filters.category] - exact match on item.category
|
|
@@ -643,3 +784,200 @@ export function runExpiry({ warnDays = 14, expireDays = 30 } = {}) {
|
|
|
643
784
|
|
|
644
785
|
return { warned, expired };
|
|
645
786
|
}
|
|
787
|
+
|
|
788
|
+
// --- CLI ---
|
|
789
|
+
//
|
|
790
|
+
// watchtower-queue.mjs is primarily a LIBRARY (every other caller `import`s it).
|
|
791
|
+
// It also exposes ONE operator-invoked subcommand — `resolve` — so a qa-handoff
|
|
792
|
+
// verdict can be stamped from a JSON file instead of an inline
|
|
793
|
+
// `node -e '<...>'` whose single-quoted string the shell breaks on apostrophes
|
|
794
|
+
// in justification text (that hazard hit the drain station EVERY time;
|
|
795
|
+
// act:00030e1d). The verdict file is the FULL resolveItem params object
|
|
796
|
+
// {resolution, resolution_type, resolution_notes, qa_verdict} — apostrophes
|
|
797
|
+
// live in resolution_notes AND qa_verdict.confessed_gap.deferred[].justification,
|
|
798
|
+
// so a verdict-only file would leave notes inline and the hazard would survive.
|
|
799
|
+
// The qa_verdict SHAPE itself is single-sourced in the qa-handoff skill's
|
|
800
|
+
// "The recipient gate"; this CLI adds no validation — it reuses resolveItem
|
|
801
|
+
// (and its validateQaVerdict gate) verbatim.
|
|
802
|
+
//
|
|
803
|
+
// Exit policy (operator-invoked → fail LOUD; the INVERSE of the hook-invoked
|
|
804
|
+
// watchtower-snapshot.mjs, which swallows everything with exit 0):
|
|
805
|
+
// 0 resolved (or resolved-but-could-not-read-back-to-confirm)
|
|
806
|
+
// 1 well-formed call could not proceed: item not found, item not pending,
|
|
807
|
+
// or the recipient gate rejected the verdict (item left pending)
|
|
808
|
+
// 2 malformed INVOCATION: bad args, missing/unreadable/non-JSON/non-object
|
|
809
|
+
// verdict file, or an unknown top-level key
|
|
810
|
+
// NOTE: this 1/2 mapping is the OPPOSITE of the sibling watchtower-lib.mjs CLI
|
|
811
|
+
// (there 1=usage, 2=operational). The taxonomy here follows the sysexits /
|
|
812
|
+
// argparse convention — 2 means the invocation itself is malformed — and the
|
|
813
|
+
// divergence from watchtower-lib is deliberate, not an oversight.
|
|
814
|
+
|
|
815
|
+
const RESOLVE_PARAM_KEYS = ['resolution', 'resolution_type', 'resolution_notes', 'qa_verdict'];
|
|
816
|
+
|
|
817
|
+
class UsageError extends Error {}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Parse `resolve <id> --verdict-file <path>` argv into {id, verdictFile}.
|
|
821
|
+
* Pure (no I/O) so it is unit-testable. Throws UsageError on any malformed
|
|
822
|
+
* form — a missing value, an id that looks like a flag, a repeated or unknown
|
|
823
|
+
* flag, or an extra positional — so the CLI maps every one to exit 2 rather
|
|
824
|
+
* than silently consuming it and failing later. Accepts both
|
|
825
|
+
* `--verdict-file <path>` and `--verdict-file=<path>`.
|
|
826
|
+
* @param {string[]} argv full process.argv ([node, script, 'resolve', ...])
|
|
827
|
+
* @returns {{id: string, verdictFile: string}}
|
|
828
|
+
*/
|
|
829
|
+
export function parseResolveArgs(argv) {
|
|
830
|
+
const args = argv.slice(3); // drop [node, script, 'resolve']
|
|
831
|
+
let id = null;
|
|
832
|
+
let verdictFile = null;
|
|
833
|
+
for (let i = 0; i < args.length; i++) {
|
|
834
|
+
const a = args[i];
|
|
835
|
+
if (a === '--verdict-file' || a.startsWith('--verdict-file=')) {
|
|
836
|
+
if (verdictFile !== null) throw new UsageError('--verdict-file given more than once');
|
|
837
|
+
if (a.startsWith('--verdict-file=')) {
|
|
838
|
+
verdictFile = a.slice('--verdict-file='.length);
|
|
839
|
+
} else {
|
|
840
|
+
verdictFile = args[i + 1];
|
|
841
|
+
i++;
|
|
842
|
+
}
|
|
843
|
+
if (typeof verdictFile !== 'string' || verdictFile === '' || verdictFile.startsWith('-')) {
|
|
844
|
+
throw new UsageError('--verdict-file requires a path argument');
|
|
845
|
+
}
|
|
846
|
+
} else if (a.startsWith('-')) {
|
|
847
|
+
throw new UsageError(`unknown flag: ${a}`);
|
|
848
|
+
} else if (id === null) {
|
|
849
|
+
id = a;
|
|
850
|
+
} else {
|
|
851
|
+
throw new UsageError(`unexpected extra argument: ${a}`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (id === null || id.trim() === '') throw new UsageError('missing <id> (the inbox item id, e.g. dec-abcd1234)');
|
|
855
|
+
if (verdictFile === null) throw new UsageError('missing --verdict-file <path>');
|
|
856
|
+
return { id, verdictFile };
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Run the `resolve` subcommand: read the verdict-params JSON file, call
|
|
861
|
+
* resolveItem, then read the item back from disk to confirm the stamp. Returns
|
|
862
|
+
* the process exit code per the policy above; never throws (each failure mode
|
|
863
|
+
* is mapped to a code with a distinct, non-misleading message).
|
|
864
|
+
* @param {string[]} argv full process.argv
|
|
865
|
+
* @returns {number} exit code
|
|
866
|
+
*/
|
|
867
|
+
export function runResolveCli(argv) {
|
|
868
|
+
let id;
|
|
869
|
+
let verdictFile;
|
|
870
|
+
try {
|
|
871
|
+
({ id, verdictFile } = parseResolveArgs(argv));
|
|
872
|
+
} catch (err) {
|
|
873
|
+
process.stderr.write(`resolve: ${err.message}\n`);
|
|
874
|
+
process.stderr.write('usage: watchtower-queue.mjs resolve <id> --verdict-file <path.json>\n');
|
|
875
|
+
return 2;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
let raw;
|
|
879
|
+
try {
|
|
880
|
+
raw = readFileSync(verdictFile, 'utf8');
|
|
881
|
+
} catch (err) {
|
|
882
|
+
process.stderr.write(err.code === 'ENOENT'
|
|
883
|
+
? `resolve: verdict file not found: ${verdictFile}\n`
|
|
884
|
+
: `resolve: cannot read verdict file ${verdictFile}: ${err.message}\n`);
|
|
885
|
+
return 2;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
let params;
|
|
889
|
+
try {
|
|
890
|
+
params = JSON.parse(raw);
|
|
891
|
+
} catch (err) {
|
|
892
|
+
process.stderr.write(`resolve: invalid JSON in ${verdictFile}: ${err.message}\n`);
|
|
893
|
+
return 2;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// typeof null === 'object' AND typeof [] === 'object' — guard both explicitly,
|
|
897
|
+
// or an array/null file would reach resolveItem and mis-resolve (exit 1)
|
|
898
|
+
// instead of being caught here as a malformed invocation (exit 2).
|
|
899
|
+
if (params === null || typeof params !== 'object' || Array.isArray(params)) {
|
|
900
|
+
process.stderr.write(`resolve: ${verdictFile} must be a JSON object of resolve params {${RESOLVE_PARAM_KEYS.join(', ')}}\n`);
|
|
901
|
+
return 2;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// resolveItem destructures exactly the 4 RESOLVE_PARAM_KEYS and JSON.stringify
|
|
905
|
+
// drops undefined — so a typo'd key (e.g. resolution_note) would be SILENTLY
|
|
906
|
+
// dropped, leaving the item resolved with a missing note and no warning. The
|
|
907
|
+
// non-qa path has no gate at all. Reject unknown keys here so a typo fails
|
|
908
|
+
// loud instead of vanishing.
|
|
909
|
+
const unknown = Object.keys(params).filter((k) => !RESOLVE_PARAM_KEYS.includes(k));
|
|
910
|
+
if (unknown.length > 0) {
|
|
911
|
+
process.stderr.write(`resolve: unknown key(s) in ${verdictFile}: ${unknown.join(', ')} — allowed: ${RESOLVE_PARAM_KEYS.join(', ')} (an unrecognized key is silently dropped, so this is rejected)\n`);
|
|
912
|
+
return 2;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
let item;
|
|
916
|
+
try {
|
|
917
|
+
item = resolveItem(id, params);
|
|
918
|
+
} catch (err) {
|
|
919
|
+
// Three distinct throw sources, and only the gate's message is safe to echo:
|
|
920
|
+
if (err.code === 'ENOENT') {
|
|
921
|
+
// missing item file — friendly, and never leak the absolute queue path.
|
|
922
|
+
process.stderr.write(`resolve: item ${id} not found\n`);
|
|
923
|
+
} else if (typeof err.message === 'string' && err.message.startsWith('qa-handoff gate:')) {
|
|
924
|
+
// recipient-gate rejection (gateError) — names the offending field and
|
|
925
|
+
// embeds NO item content or path, so surface it verbatim. The item is
|
|
926
|
+
// left pending (validateQaVerdict throws before the atomicWrite).
|
|
927
|
+
process.stderr.write(`resolve: ${err.message}\n`);
|
|
928
|
+
} else {
|
|
929
|
+
// anything else — a corrupt/wrong-schema item file (readItem) or a write
|
|
930
|
+
// failure (atomicWrite) — embeds the ABSOLUTE queue path in err.message.
|
|
931
|
+
// Do NOT echo it; name only the id and the error code.
|
|
932
|
+
process.stderr.write(`resolve: could not resolve item ${id} (${err.code || 'unexpected error'})\n`);
|
|
933
|
+
}
|
|
934
|
+
return 1;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (item === null) {
|
|
938
|
+
let status = 'not pending';
|
|
939
|
+
try {
|
|
940
|
+
const cur = getItem(id);
|
|
941
|
+
if (cur && cur.status) status = cur.status;
|
|
942
|
+
} catch { /* best-effort status lookup for the message only */ }
|
|
943
|
+
process.stderr.write(`resolve: ${id} is not pending (status: ${status}) — an earlier stamp is intact, nothing to do\n`);
|
|
944
|
+
return 1;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Verify the write by re-reading from disk, not trusting the in-memory return
|
|
948
|
+
// (verify-your-own-writes). A read-back failure must NOT read as a resolve
|
|
949
|
+
// failure — the stamp already landed (resolveItem's atomicWrite returned).
|
|
950
|
+
try {
|
|
951
|
+
const stored = getItem(id);
|
|
952
|
+
const token = stored && stored.qa_verdict && stored.qa_verdict.verdict
|
|
953
|
+
? stored.qa_verdict.verdict
|
|
954
|
+
: (stored && stored.resolution) || '(resolved)';
|
|
955
|
+
const notes = stored && stored.resolution_notes ? 'present' : 'absent';
|
|
956
|
+
process.stdout.write(`resolved ${id} — verdict: ${token}; resolution_notes: ${notes}\n`);
|
|
957
|
+
} catch (err) {
|
|
958
|
+
process.stdout.write(`resolved ${id}, but could not read it back to confirm: ${err.message}\n`);
|
|
959
|
+
}
|
|
960
|
+
return 0;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Entry guard — importing this module must NOT run the CLI (mirrors the ring
|
|
964
|
+
// scripts and watchtower-snapshot.mjs so tests can import the pure functions).
|
|
965
|
+
const isMain = (() => {
|
|
966
|
+
try {
|
|
967
|
+
return process.argv[1]
|
|
968
|
+
&& import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
|
|
969
|
+
} catch {
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
})();
|
|
973
|
+
|
|
974
|
+
if (isMain) {
|
|
975
|
+
const sub = process.argv[2];
|
|
976
|
+
if (sub === 'resolve') {
|
|
977
|
+
process.exit(runResolveCli(process.argv));
|
|
978
|
+
} else {
|
|
979
|
+
process.stderr.write(`watchtower-queue.mjs: unknown command ${sub ? `'${sub}'` : '(none)'}\n`);
|
|
980
|
+
process.stderr.write('usage: watchtower-queue.mjs resolve <id> --verdict-file <path.json>\n');
|
|
981
|
+
process.exit(2);
|
|
982
|
+
}
|
|
983
|
+
}
|