@holmes-lab/holmes-kit 0.19.6 → 0.20.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/CHANGELOG.md +96 -0
- package/README.md +8 -1
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve-status.d.ts +24 -0
- package/dist/holmes/cli/approve-status.js +166 -0
- package/dist/holmes/cli/approve-watch.d.ts +6 -0
- package/dist/holmes/cli/approve-watch.js +6 -0
- package/dist/holmes/cli/approve.d.ts +27 -0
- package/dist/holmes/cli/approve.js +68 -7
- package/dist/holmes/cli/cli-execution.d.ts +2 -0
- package/dist/holmes/cli/cli-execution.js +24 -0
- package/dist/holmes/cli/gitignore-merge.d.ts +0 -5
- package/dist/holmes/cli/gitignore-merge.js +6 -2
- package/dist/holmes/cli/index.js +176 -139
- package/dist/holmes/cli/playbook-skills.d.ts +2 -22
- package/dist/holmes/cli/playbook-skills.js +10 -54
- package/dist/holmes/governance/approval-grants.d.ts +22 -0
- package/dist/holmes/governance/approval-grants.js +55 -1
- package/dist/holmes/governance/approval-queue.d.ts +43 -4
- package/dist/holmes/governance/approval-queue.js +86 -12
- package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
- package/dist/holmes/governance/display-id-aliases.js +86 -0
- package/dist/holmes/governance/ledger-store.d.ts +42 -14
- package/dist/holmes/governance/ledger-store.js +49 -3
- package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
- package/dist/holmes/governance/ledger-timeline.js +11 -1
- package/dist/holmes/governance/provenance-chain.d.ts +2 -0
- package/dist/holmes/governance/provenance-chain.js +13 -2
- package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
- package/dist/holmes/governance/provenance-ledger.js +69 -16
- package/dist/holmes/governance/session-context.d.ts +3 -0
- package/dist/holmes/governance/session-context.js +26 -8
- package/dist/holmes/guardrail/risk-classifier.js +2 -2
- package/dist/holmes/guardrail/write-target.js +2 -1
- package/dist/holmes/hooks/pre-tool-use.js +12 -1
- package/dist/holmes/hooks/stop.js +14 -3
- package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
- package/dist/holmes/mcp/handlers/adoption.js +186 -0
- package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
- package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
- package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
- package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
- package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
- package/dist/holmes/mcp/handlers/entity-store.js +212 -0
- package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
- package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
- package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
- package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
- package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
- package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
- package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
- package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
- package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
- package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
- package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
- package/dist/holmes/mcp/handlers/review-queries.js +79 -0
- package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
- package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +55 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.js +324 -0
- package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
- package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
- package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
- package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
- package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
- package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
- package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
- package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
- package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
- package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
- package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
- package/dist/holmes/mcp/handlers/test-execution.js +210 -0
- package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
- package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
- package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
- package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
- package/dist/holmes/mcp/handlers.d.ts +653 -424
- package/dist/holmes/mcp/handlers.js +267 -3250
- package/dist/holmes/mcp/server.js +2 -1
- package/dist/holmes/mcp/tool-schemas.js +43 -2
- package/dist/holmes/project/execution-context.d.ts +17 -0
- package/dist/holmes/project/execution-context.js +120 -0
- package/dist/holmes/project/installer-markers.d.ts +34 -0
- package/dist/holmes/project/installer-markers.js +65 -0
- package/dist/holmes/project/root.d.ts +3 -1
- package/dist/holmes/project/root.js +4 -3
- package/dist/holmes/project/workspace-identity.d.ts +29 -0
- package/dist/holmes/project/workspace-identity.js +181 -0
- package/dist/holmes/review/test-outcomes.d.ts +6 -1
- package/dist/holmes/review/test-outcomes.js +23 -5
- package/dist/holmes/review/test-runner.d.ts +18 -0
- package/dist/holmes/review/test-runner.js +132 -4
- package/dist/holmes/rtm/graph-store.d.ts +50 -1
- package/dist/holmes/rtm/graph-store.js +244 -3
- package/dist/holmes/rtm/incremental.d.ts +1 -0
- package/dist/holmes/rtm/incremental.js +12 -3
- package/dist/holmes/rtm/localize.js +7 -0
- package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
- package/dist/holmes/rtm/rtm-graph.js +13 -0
- package/dist/holmes/semantic/vector-cache.d.ts +16 -1
- package/dist/holmes/semantic/vector-cache.js +155 -19
- package/dist/holmes/spec/approval-status.d.ts +10 -0
- package/dist/holmes/spec/approval-status.js +7 -3
- package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
- package/dist/holmes/spec/entity-git-snapshot.js +276 -0
- package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
- package/dist/holmes/spec/entity-integration-discharge.js +233 -0
- package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
- package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
- package/dist/holmes/spec/entity-integration.d.ts +207 -0
- package/dist/holmes/spec/entity-integration.js +747 -0
- package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
- package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
- package/dist/holmes/spec/entity-renumber.d.ts +32 -0
- package/dist/holmes/spec/entity-renumber.js +156 -0
- package/dist/holmes/spec/entity-store.d.ts +135 -0
- package/dist/holmes/spec/entity-store.js +1051 -0
- package/dist/holmes/spec/entity-transaction.d.ts +89 -0
- package/dist/holmes/spec/entity-transaction.js +701 -0
- package/dist/holmes/spec/renumber.d.ts +58 -0
- package/dist/holmes/spec/renumber.js +200 -1
- package/dist/holmes/spec/spec-store.d.ts +3 -2
- package/dist/holmes/spec/spec-store.js +23 -2
- package/dist/holmes/spec/transition-policy.d.ts +36 -0
- package/dist/holmes/spec/transition-policy.js +135 -0
- package/package.json +1 -1
package/dist/holmes/cli/index.js
CHANGED
|
@@ -37,7 +37,8 @@ exports.isBrokenPipe = void 0;
|
|
|
37
37
|
exports.packageRoot = packageRoot;
|
|
38
38
|
exports.main = main;
|
|
39
39
|
exports.installPipeGuard = installPipeGuard;
|
|
40
|
-
// @implements A-SPEC-591
|
|
40
|
+
// @implements A-SPEC-591, A-SPEC-626
|
|
41
|
+
const cli_execution_1 = require("./cli-execution");
|
|
41
42
|
// @implements A-SPEC-100.2
|
|
42
43
|
// @implements A-SPEC-213
|
|
43
44
|
// @implements A-SPEC-215
|
|
@@ -79,7 +80,7 @@ const KNOWN_FLAGS = {
|
|
|
79
80
|
skills: ['help', 'target'],
|
|
80
81
|
ci: ['help', 'target', 'specs-dir', 'json'],
|
|
81
82
|
serve: ['help', 'target', 'port'],
|
|
82
|
-
approve: ['help', 'target', 'list', 'refusals', 'grant', 'deny', 'ask', 'reason', 'question', 'ttl', 'rationale', 'watch', 'poll-ms'],
|
|
83
|
+
approve: ['help', 'target', 'list', 'status', 'refusals', 'grant', 'deny', 'ask', 'revoke', 'reason', 'question', 'ttl', 'rationale', 'watch', 'poll-ms'], // A-SPEC-649: --revoke · A-SPEC-650: --status
|
|
83
84
|
ledger: ['help', 'target', 'ref', 'dry-run'],
|
|
84
85
|
// @implements A-SPEC-477 — the human's opt-in act for the cloud semantic tier.
|
|
85
86
|
'semantic-key': ['help'],
|
|
@@ -101,10 +102,12 @@ const APPROVE_USAGE = `holmes-kit approve — decide pending approval requests (
|
|
|
101
102
|
interactive (TTY): per item [a] approve [e] approve with reason/duration [v] details [d] deny [q] ask [s] skip [A] approve all remaining
|
|
102
103
|
--watch resident mode — decide as requests arrive, wait when the queue is empty (Ctrl-C to exit, --poll-ms <ms>)
|
|
103
104
|
--list print the pending list only
|
|
105
|
+
--status the whole picture: workspace, delegation, who asked and how risky, recent outcomes
|
|
104
106
|
--grant <ref> issue a narrow grant for that request (--ttl <minutes>, --rationale <text>)
|
|
105
107
|
ref = list number (1,2,…) | id | id prefix — no need to retype a long id
|
|
106
108
|
--deny <id> --reason <text> deny — the reason rides the agent's next refusal
|
|
107
109
|
--ask <id> --question <text> leave a question and hold — the agent relays it to the user
|
|
110
|
+
--revoke <id> --reason <text> withdraw a live grant — the request must be decided again
|
|
108
111
|
--target <dir> project root (default: current directory)
|
|
109
112
|
`;
|
|
110
113
|
class UnknownFlagError extends Error {
|
|
@@ -115,7 +118,7 @@ class UnknownFlagError extends Error {
|
|
|
115
118
|
* what you'd do" spelling performed a REAL write (round-3 HIGH; same class inverted --remove and
|
|
116
119
|
* --force). A boolean flag must never consume a token.
|
|
117
120
|
*/
|
|
118
|
-
const BOOLEAN_FLAGS = new Set(['help', 'dry-run', 'no-mcp', 'remove', 'force', 'json', 'list', 'watch', 'autonomy', 'no-autonomy']);
|
|
121
|
+
const BOOLEAN_FLAGS = new Set(['help', 'dry-run', 'no-mcp', 'remove', 'force', 'json', 'list', 'status', 'watch', 'autonomy', 'no-autonomy']); // A-SPEC-650: --status
|
|
119
122
|
function parseFlags(argv, known) {
|
|
120
123
|
const flags = {};
|
|
121
124
|
const positionals = [];
|
|
@@ -209,9 +212,11 @@ const USAGE = `holmes-kit — deterministic ASE governance for a project
|
|
|
209
212
|
holmes-kit approve review pending approval requests (HITL)
|
|
210
213
|
(no flags, TTY) interactive: [a]approve [e]approve-with-terms [v]details [d]deny [q]ask [s]skip [A]approve-all
|
|
211
214
|
--list print the pending queue and exit
|
|
215
|
+
--status workspace · delegation · per-request risk/run/subject · recent outcomes
|
|
212
216
|
--grant <ref> [--ttl <min>] [--rationale <text>] mint the narrow grant for one request
|
|
213
217
|
--deny <ref> --reason <text> record a denial the agent will see
|
|
214
218
|
--ask <ref> --question <text> hold with a question the agent relays
|
|
219
|
+
--revoke <id> --reason <text> withdraw a live grant (full id or unique prefix)
|
|
215
220
|
<ref> list index (1,2,…), full id, or a unique id prefix
|
|
216
221
|
--target <dir> project root (default: cwd)
|
|
217
222
|
|
|
@@ -331,13 +336,15 @@ async function main(argv) {
|
|
|
331
336
|
const { runAutonomy } = await Promise.resolve().then(() => __importStar(require('./autonomy')));
|
|
332
337
|
const root = path.resolve(typeof flags.target === 'string' ? flags.target : process.cwd());
|
|
333
338
|
const forStr = typeof flags.for === 'string' ? flags.for : undefined;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
339
|
+
return (0, cli_execution_1.runOperatorExecution)(root, () => {
|
|
340
|
+
const r = runAutonomy(root, positionals[0], forStr, {
|
|
341
|
+
now: new Date().toISOString(),
|
|
342
|
+
isTTY: !!process.stdin.isTTY && !!process.stdout.isTTY,
|
|
343
|
+
approval: readApproval(),
|
|
344
|
+
});
|
|
345
|
+
process[r.ok ? 'stdout' : 'stderr'].write(`${r.message}\n`);
|
|
346
|
+
return r.exitCode;
|
|
338
347
|
});
|
|
339
|
-
process[r.ok ? 'stdout' : 'stderr'].write(`${r.message}\n`);
|
|
340
|
-
return r.exitCode;
|
|
341
348
|
}
|
|
342
349
|
if (cmd === 'doctor') {
|
|
343
350
|
// A nonexistent target read as "healthy but unwired" and both advised remedies then refused it
|
|
@@ -799,158 +806,188 @@ async function main(argv) {
|
|
|
799
806
|
process.stdout.write(APPROVE_USAGE);
|
|
800
807
|
return 0;
|
|
801
808
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
809
|
+
return (0, cli_execution_1.runOperatorExecution)(root, async () => {
|
|
810
|
+
if (typeof flags.grant === 'string') {
|
|
811
|
+
const opts = { actor };
|
|
812
|
+
if (typeof flags.ttl === 'string')
|
|
813
|
+
opts.ttlMinutes = Number(flags.ttl);
|
|
814
|
+
if (typeof flags.rationale === 'string')
|
|
815
|
+
opts.rationale = flags.rationale;
|
|
816
|
+
const ref = resolveRef(flags.grant);
|
|
817
|
+
if ('refusal' in ref) {
|
|
818
|
+
process.stdout.write(`\u2717 ${ref.refusal}\n`);
|
|
819
|
+
return 1;
|
|
820
|
+
}
|
|
821
|
+
const r = grantRequest(root, ref.id, opts);
|
|
822
|
+
process.stdout.write(r.ok ? `✓ approved — ${ref.subject('✓ approved — ')}\n${ref.detail(`valid until ${r.expires}`)}\n` : `✗ ${r.reason}\n`);
|
|
823
|
+
return r.ok ? 0 : 1;
|
|
812
824
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
825
|
+
if (typeof flags.deny === 'string') {
|
|
826
|
+
const reason = typeof flags.reason === 'string' ? flags.reason.trim() : '';
|
|
827
|
+
if (reason === '') {
|
|
828
|
+
process.stderr.write('✗ --deny requires --reason — a denial with no reason is invisible to the agent\n');
|
|
829
|
+
return 1;
|
|
830
|
+
}
|
|
831
|
+
const ref = resolveRef(flags.deny);
|
|
832
|
+
if ('refusal' in ref) {
|
|
833
|
+
process.stdout.write(`\u2717 ${ref.refusal}\n`);
|
|
834
|
+
return 1;
|
|
835
|
+
}
|
|
836
|
+
const r = denyRequest(root, ref.id, reason, actor);
|
|
837
|
+
process.stdout.write(r.ok ? `✓ denied — ${ref.subject('✓ denied — ')}\n${ref.detail('the denial was recorded')}\n` : `✗ ${r.reason}\n`);
|
|
838
|
+
return r.ok ? 0 : 1;
|
|
822
839
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
840
|
+
// @implements A-SPEC-649 — the operator withdraws a live grant. A granted id is no longer
|
|
841
|
+
// pending, so it resolves against the DECIDED ids (full id or a unique prefix), never a list index.
|
|
842
|
+
if (typeof flags.revoke === 'string') {
|
|
843
|
+
const reason = typeof flags.reason === 'string' ? flags.reason.trim() : '';
|
|
844
|
+
if (reason === '') {
|
|
845
|
+
process.stderr.write('✗ --revoke requires --reason — a revocation with no reason is invisible to the agent\n');
|
|
846
|
+
return 1;
|
|
847
|
+
}
|
|
848
|
+
const { revokeGrant } = require('./approve');
|
|
849
|
+
const ref = String(flags.revoke).trim();
|
|
850
|
+
const granted = Object.entries(readQueue(root, { includeAllKinds: true }).decisions).filter(([, d]) => d.event === 'granted').map(([id]) => id);
|
|
851
|
+
const matches = granted.includes(ref) ? [ref] : granted.filter((id) => id.startsWith(ref));
|
|
852
|
+
if (matches.length !== 1) {
|
|
853
|
+
process.stdout.write(`\u2717 ${matches.length === 0 ? `no granted request matches ${ref}` : `${ref} matches ${matches.length} granted requests — use the full id`}\n`);
|
|
854
|
+
return 1;
|
|
855
|
+
}
|
|
856
|
+
const r = revokeGrant(root, matches[0], reason, actor);
|
|
857
|
+
process.stdout.write(r.ok ? `✓ revoked — ${matches[0]}\n the grant is withdrawn; the agent's next attempt is refused and re-filed\n` : `✗ ${r.reason}\n`);
|
|
858
|
+
return r.ok ? 0 : 1;
|
|
827
859
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
860
|
+
if (typeof flags.ask === 'string') {
|
|
861
|
+
const question = typeof flags.question === 'string' ? flags.question.trim() : '';
|
|
862
|
+
if (question === '') {
|
|
863
|
+
process.stderr.write('✗ --ask requires --question\n');
|
|
864
|
+
return 1;
|
|
865
|
+
}
|
|
866
|
+
const ref = resolveRef(flags.ask);
|
|
867
|
+
if ('refusal' in ref) {
|
|
868
|
+
process.stdout.write(`\u2717 ${ref.refusal}\n`);
|
|
869
|
+
return 1;
|
|
870
|
+
}
|
|
871
|
+
const r = holdRequest(root, ref.id, question, actor);
|
|
872
|
+
process.stdout.write(r.ok ? `✓ held — ${ref.subject('✓ held — ')}\n${ref.detail('the question rides the next refusal')}\n` : `✗ ${r.reason}\n`);
|
|
873
|
+
return r.ok ? 0 : 1;
|
|
837
874
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
875
|
+
// @implements A-SPEC-507.1 — stale folding is a LIST-VIEW judgment: the clock enters only
|
|
876
|
+
// here, never on gate-path folds. `--stale-hours 0` disables it; `--all` unfolds.
|
|
877
|
+
const { staleView } = require('./approve');
|
|
878
|
+
const stale = staleView(flags);
|
|
879
|
+
const queueOpts = stale ? { now: Date.now(), ttlMs: stale.ttlMs } : undefined;
|
|
880
|
+
const listView = stale ? { staleHours: stale.ttlMs / 3_600_000, showAll: stale.showAll } : undefined;
|
|
881
|
+
if (flags.list) {
|
|
882
|
+
process.stdout.write(renderPending(readQueue(root, queueOpts), listView) + '\n');
|
|
883
|
+
return 0;
|
|
842
884
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
// here, never on gate-path folds. `--stale-hours 0` disables it; `--all` unfolds.
|
|
849
|
-
const { staleView } = require('./approve');
|
|
850
|
-
const stale = staleView(flags);
|
|
851
|
-
const queueOpts = stale ? { now: Date.now(), ttlMs: stale.ttlMs } : undefined;
|
|
852
|
-
const listView = stale ? { staleHours: stale.ttlMs / 3_600_000, showAll: stale.showAll } : undefined;
|
|
853
|
-
if (flags.list) {
|
|
854
|
-
process.stdout.write(renderPending(readQueue(root, queueOpts), listView) + '\n');
|
|
855
|
-
return 0;
|
|
856
|
-
}
|
|
857
|
-
// @implements A-SPEC-563.2 — the read-only refusal view (local log; raw targets stay on-screen).
|
|
858
|
-
if (flags.refusals !== undefined) {
|
|
859
|
-
const { renderRefusals } = require('./approve');
|
|
860
|
-
const { readRefusals } = require('../governance/approval-queue');
|
|
861
|
-
const limit = typeof flags.refusals === 'string' ? Number(flags.refusals) : 20;
|
|
862
|
-
if (!Number.isInteger(limit) || limit <= 0) {
|
|
863
|
-
process.stderr.write(`✗ --refusals expects a positive integer count — received: ${flags.refusals}\n`);
|
|
864
|
-
return 1;
|
|
885
|
+
// @implements A-SPEC-650 — the whole picture, read-only; the list above stays script-clean.
|
|
886
|
+
if (flags.status) {
|
|
887
|
+
const { renderStatus, statusContext } = require('./approve-status');
|
|
888
|
+
process.stdout.write(renderStatus(readQueue(root), statusContext(root)) + '\n');
|
|
889
|
+
return 0;
|
|
865
890
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
891
|
+
// @implements A-SPEC-563.2 — the read-only refusal view (local log; raw targets stay on-screen).
|
|
892
|
+
if (flags.refusals !== undefined) {
|
|
893
|
+
const { renderRefusals } = require('./approve');
|
|
894
|
+
const { readRefusals } = require('../governance/approval-queue');
|
|
895
|
+
const limit = typeof flags.refusals === 'string' ? Number(flags.refusals) : 20;
|
|
896
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
897
|
+
process.stderr.write(`✗ --refusals expects a positive integer count — received: ${flags.refusals}\n`);
|
|
898
|
+
return 1;
|
|
899
|
+
}
|
|
900
|
+
process.stdout.write(renderRefusals(readRefusals(root), limit) + '\n');
|
|
901
|
+
return 0;
|
|
902
|
+
}
|
|
903
|
+
// @implements A-SPEC-262.2 — the RESIDENT surface. Validate --poll-ms first (so a bad value is
|
|
904
|
+
// named even on a non-TTY), then refuse a non-TTY (a resident interactive loop needs a terminal),
|
|
905
|
+
// then run the loop. SIGINT and readline close both abort it — the operator leaving, either way,
|
|
906
|
+
// ends the watch at once and exit 0; pending items stay in the queue.
|
|
907
|
+
if (flags.watch === true) {
|
|
908
|
+
let pollMs;
|
|
909
|
+
if (typeof flags['poll-ms'] === 'string') {
|
|
910
|
+
pollMs = Number(flags['poll-ms']);
|
|
911
|
+
if (!Number.isInteger(pollMs) || pollMs <= 0) {
|
|
912
|
+
process.stderr.write(`✗ --poll-ms must be a positive integer (milliseconds) — received: ${flags['poll-ms']}\n`);
|
|
913
|
+
return 1;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (!process.stdin.isTTY) {
|
|
917
|
+
process.stderr.write('✗ interactive resident mode (--watch) requires a TTY — use --list in scripts\n');
|
|
879
918
|
return 1;
|
|
880
919
|
}
|
|
920
|
+
// @implements A-SPEC-497.2 — the resident surface decides on ONE raw keypress (a/d/q/s, no
|
|
921
|
+
// Enter, no deadline) and pings the OS on each new arrival. Raw mode is restored on the way
|
|
922
|
+
// out whatever happens; Ctrl-C arrives as \x03 through decideByKey and ends the loop.
|
|
923
|
+
const { runWatch } = require('./approve-watch');
|
|
924
|
+
const { notifyQueueArrival } = require('./os-notify');
|
|
925
|
+
const ac = new AbortController();
|
|
926
|
+
const onSig = () => ac.abort();
|
|
927
|
+
process.once('SIGINT', onSig);
|
|
928
|
+
const stdin = process.stdin;
|
|
929
|
+
try {
|
|
930
|
+
stdin.setRawMode?.(true);
|
|
931
|
+
stdin.resume();
|
|
932
|
+
await runWatch(root, {
|
|
933
|
+
print: (t) => process.stdout.write(t + '\n'),
|
|
934
|
+
ask: async () => '', // unused on the key surface; kept for the ApproveIO shape
|
|
935
|
+
}, actor, {
|
|
936
|
+
signal: ac.signal,
|
|
937
|
+
keyStream: stdin,
|
|
938
|
+
notify: (p) => notifyQueueArrival(p),
|
|
939
|
+
// @implements A-SPEC-650 — the status header before each new-item pass.
|
|
940
|
+
status: () => { const { renderStatus, statusContext } = require('./approve-status'); return renderStatus(readQueue(root), statusContext(root)); },
|
|
941
|
+
...(pollMs !== undefined ? { pollMs } : {}),
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
catch (e) {
|
|
945
|
+
if (e?.name !== 'AbortError')
|
|
946
|
+
throw e;
|
|
947
|
+
}
|
|
948
|
+
finally {
|
|
949
|
+
process.removeListener('SIGINT', onSig);
|
|
950
|
+
stdin.setRawMode?.(false);
|
|
951
|
+
stdin.pause();
|
|
952
|
+
}
|
|
953
|
+
return 0;
|
|
881
954
|
}
|
|
955
|
+
// @implements A-SPEC-260 — the implicit non-TTY fallback tells the operator the next command,
|
|
956
|
+
// with the real pending id filled in; the explicit --list above stays script-clean.
|
|
882
957
|
if (!process.stdin.isTTY) {
|
|
883
|
-
|
|
884
|
-
|
|
958
|
+
const state = readQueue(root, queueOpts);
|
|
959
|
+
const hint = renderNonTtyHint(state);
|
|
960
|
+
process.stdout.write(renderPending(state, listView) + (hint ? '\n\n' + hint : '') + '\n');
|
|
961
|
+
return 0;
|
|
885
962
|
}
|
|
886
|
-
// @implements A-SPEC-
|
|
887
|
-
//
|
|
888
|
-
//
|
|
889
|
-
|
|
890
|
-
|
|
963
|
+
// @implements A-SPEC-262.1
|
|
964
|
+
// AWAITED, not fire-and-forget. Measured 2026-08-25 against the shipped bin with a real pty: the
|
|
965
|
+
// prompt printed and the session ended with zero grants, because `void runInteractive(...)`
|
|
966
|
+
// returned 0 immediately and bin/holmes-kit.js turns that into `process.exit(0)` — killing the
|
|
967
|
+
// loop before the first keypress. The decision surface REQ-246 shipped had never run for a real
|
|
968
|
+
// operator; every test called `runInteractive` directly and so never crossed this seam.
|
|
969
|
+
const rl = require('node:readline/promises').createInterface({ input: process.stdin, output: process.stdout });
|
|
970
|
+
// EOF is the operator LEAVING (Ctrl-D, a closed pipe). Node leaves a pending `question`
|
|
971
|
+
// unsettled when the interface closes, so without this the awaited loop would hang the command
|
|
972
|
+
// forever — measured 2026-08-25, a pty probe hung for five minutes. The abort ends the session
|
|
973
|
+
// the way the operator asked: quietly, exit 0, nothing decided that was not decided.
|
|
891
974
|
const ac = new AbortController();
|
|
892
|
-
|
|
893
|
-
process.once('SIGINT', onSig);
|
|
894
|
-
const stdin = process.stdin;
|
|
975
|
+
rl.once('close', () => ac.abort());
|
|
895
976
|
try {
|
|
896
|
-
|
|
897
|
-
stdin.resume();
|
|
898
|
-
await runWatch(root, {
|
|
977
|
+
await runInteractive(root, {
|
|
899
978
|
print: (t) => process.stdout.write(t + '\n'),
|
|
900
|
-
ask:
|
|
901
|
-
}, actor
|
|
902
|
-
signal: ac.signal,
|
|
903
|
-
keyStream: stdin,
|
|
904
|
-
notify: (p) => notifyQueueArrival(p),
|
|
905
|
-
...(pollMs !== undefined ? { pollMs } : {}),
|
|
906
|
-
});
|
|
979
|
+
ask: (q) => rl.question(q, { signal: ac.signal }),
|
|
980
|
+
}, actor);
|
|
907
981
|
}
|
|
908
982
|
catch (e) {
|
|
909
983
|
if (e?.name !== 'AbortError')
|
|
910
984
|
throw e;
|
|
911
985
|
}
|
|
912
986
|
finally {
|
|
913
|
-
|
|
914
|
-
stdin.setRawMode?.(false);
|
|
915
|
-
stdin.pause();
|
|
987
|
+
rl.close();
|
|
916
988
|
}
|
|
917
989
|
return 0;
|
|
918
|
-
}
|
|
919
|
-
// @implements A-SPEC-260 — the implicit non-TTY fallback tells the operator the next command,
|
|
920
|
-
// with the real pending id filled in; the explicit --list above stays script-clean.
|
|
921
|
-
if (!process.stdin.isTTY) {
|
|
922
|
-
const state = readQueue(root, queueOpts);
|
|
923
|
-
const hint = renderNonTtyHint(state);
|
|
924
|
-
process.stdout.write(renderPending(state, listView) + (hint ? '\n\n' + hint : '') + '\n');
|
|
925
|
-
return 0;
|
|
926
|
-
}
|
|
927
|
-
// @implements A-SPEC-262.1
|
|
928
|
-
// AWAITED, not fire-and-forget. Measured 2026-08-25 against the shipped bin with a real pty: the
|
|
929
|
-
// prompt printed and the session ended with zero grants, because `void runInteractive(...)`
|
|
930
|
-
// returned 0 immediately and bin/holmes-kit.js turns that into `process.exit(0)` — killing the
|
|
931
|
-
// loop before the first keypress. The decision surface REQ-246 shipped had never run for a real
|
|
932
|
-
// operator; every test called `runInteractive` directly and so never crossed this seam.
|
|
933
|
-
const rl = require('node:readline/promises').createInterface({ input: process.stdin, output: process.stdout });
|
|
934
|
-
// EOF is the operator LEAVING (Ctrl-D, a closed pipe). Node leaves a pending `question`
|
|
935
|
-
// unsettled when the interface closes, so without this the awaited loop would hang the command
|
|
936
|
-
// forever — measured 2026-08-25, a pty probe hung for five minutes. The abort ends the session
|
|
937
|
-
// the way the operator asked: quietly, exit 0, nothing decided that was not decided.
|
|
938
|
-
const ac = new AbortController();
|
|
939
|
-
rl.once('close', () => ac.abort());
|
|
940
|
-
try {
|
|
941
|
-
await runInteractive(root, {
|
|
942
|
-
print: (t) => process.stdout.write(t + '\n'),
|
|
943
|
-
ask: (q) => rl.question(q, { signal: ac.signal }),
|
|
944
|
-
}, actor);
|
|
945
|
-
}
|
|
946
|
-
catch (e) {
|
|
947
|
-
if (e?.name !== 'AbortError')
|
|
948
|
-
throw e;
|
|
949
|
-
}
|
|
950
|
-
finally {
|
|
951
|
-
rl.close();
|
|
952
|
-
}
|
|
953
|
-
return 0;
|
|
990
|
+
});
|
|
954
991
|
}
|
|
955
992
|
if (cmd === 'serve') {
|
|
956
993
|
if (typeof flags.target === 'string') {
|
|
@@ -11,12 +11,8 @@
|
|
|
11
11
|
* Playbooks are ADVISORY. No gate decision reads them, and a user who deletes every one of them
|
|
12
12
|
* still gets identical enforcement — which is why a missing playbook is a doctor WARN, never a FAIL.
|
|
13
13
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* is exactly what a user's own skill of the same name would occupy, and overwriting that would be
|
|
17
|
-
* the same class of error as clobbering their settings.
|
|
18
|
-
*/
|
|
19
|
-
export declare const PLAYBOOK_SKILL_MARKER = "<!-- installed by holmes-kit; edits will be replaced on upgrade -->";
|
|
14
|
+
import { PLAYBOOK_SKILL_MARKER, carriesMarker, frontmatterEnd } from '../project/installer-markers';
|
|
15
|
+
export { PLAYBOOK_SKILL_MARKER, carriesMarker, frontmatterEnd };
|
|
20
16
|
/**
|
|
21
17
|
* @implements A-SPEC-190 (round 7)
|
|
22
18
|
* The bytes an install writes. The marker used to go FIRST, which put it ahead of the playbook's
|
|
@@ -28,22 +24,6 @@ export declare const PLAYBOOK_SKILL_MARKER = "<!-- installed by holmes-kit; edit
|
|
|
28
24
|
* while doctor certified the layout as `current`. The marker now follows the frontmatter block.
|
|
29
25
|
*/
|
|
30
26
|
export declare function installedSkillBytes(body: string): string;
|
|
31
|
-
/**
|
|
32
|
-
* @implements A-SPEC-190 (round 7)
|
|
33
|
-
* Ownership is the marker AT ITS PLACE, not the string anywhere in the file. `includes` made any
|
|
34
|
-
* document that merely QUOTES the marker — documentation about holmes-kit, a review note — read as
|
|
35
|
-
* ours: doctor called it `drifted` and the refresh it advised overwrote the user's file with no
|
|
36
|
-
* backup. A quotation is not a claim of ownership.
|
|
37
|
-
*/
|
|
38
|
-
export declare function carriesMarker(content: string): boolean;
|
|
39
|
-
/**
|
|
40
|
-
* @implements A-SPEC-190 (round 8)
|
|
41
|
-
* Where a leading YAML frontmatter block ends, or null. Round-8 measured the shapes the first
|
|
42
|
-
* regex missed — a BOM, an EMPTY block (`---\n---\n`), and a closing fence at end-of-file with no
|
|
43
|
-
* trailing newline — each of which sent the marker back to byte 0, recreating the CRITICAL the
|
|
44
|
-
* round-7 fix existed to remove while doctor certified the result as `current`.
|
|
45
|
-
*/
|
|
46
|
-
export declare function frontmatterEnd(content: string): number | null;
|
|
47
27
|
/**
|
|
48
28
|
* @implements A-SPEC-172
|
|
49
29
|
* What the user types to reach a playbook.
|
|
@@ -33,10 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.PLAYBOOK_SKILL_MARKER = void 0;
|
|
36
|
+
exports.frontmatterEnd = exports.carriesMarker = exports.PLAYBOOK_SKILL_MARKER = void 0;
|
|
37
37
|
exports.installedSkillBytes = installedSkillBytes;
|
|
38
|
-
exports.carriesMarker = carriesMarker;
|
|
39
|
-
exports.frontmatterEnd = frontmatterEnd;
|
|
40
38
|
exports.invocableSkillName = invocableSkillName;
|
|
41
39
|
exports.skillPathFor = skillPathFor;
|
|
42
40
|
exports.installPlaybookSkills = installPlaybookSkills;
|
|
@@ -62,12 +60,10 @@ const path = __importStar(require("node:path"));
|
|
|
62
60
|
* Playbooks are ADVISORY. No gate decision reads them, and a user who deletes every one of them
|
|
63
61
|
* still gets identical enforcement — which is why a missing playbook is a doctor WARN, never a FAIL.
|
|
64
62
|
*/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
*/
|
|
70
|
-
exports.PLAYBOOK_SKILL_MARKER = '<!-- installed by holmes-kit; edits will be replaced on upgrade -->';
|
|
63
|
+
const installer_markers_1 = require("../project/installer-markers");
|
|
64
|
+
Object.defineProperty(exports, "PLAYBOOK_SKILL_MARKER", { enumerable: true, get: function () { return installer_markers_1.PLAYBOOK_SKILL_MARKER; } });
|
|
65
|
+
Object.defineProperty(exports, "carriesMarker", { enumerable: true, get: function () { return installer_markers_1.carriesMarker; } });
|
|
66
|
+
Object.defineProperty(exports, "frontmatterEnd", { enumerable: true, get: function () { return installer_markers_1.frontmatterEnd; } });
|
|
71
67
|
/**
|
|
72
68
|
* @implements A-SPEC-190 (round 7)
|
|
73
69
|
* The bytes an install writes. The marker used to go FIRST, which put it ahead of the playbook's
|
|
@@ -79,54 +75,14 @@ exports.PLAYBOOK_SKILL_MARKER = '<!-- installed by holmes-kit; edits will be rep
|
|
|
79
75
|
* while doctor certified the layout as `current`. The marker now follows the frontmatter block.
|
|
80
76
|
*/
|
|
81
77
|
function installedSkillBytes(body) {
|
|
82
|
-
const end = frontmatterEnd(body);
|
|
78
|
+
const end = (0, installer_markers_1.frontmatterEnd)(body);
|
|
83
79
|
if (end === null)
|
|
84
|
-
return `${
|
|
80
|
+
return `${installer_markers_1.PLAYBOOK_SKILL_MARKER}\n${body}`;
|
|
85
81
|
// The offset is computed on normalised text; a body with CRLF or a BOM is rewritten normalised so
|
|
86
82
|
// the marker cannot land inside the frontmatter it must follow.
|
|
87
83
|
const text = body.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
|
88
84
|
const head = text.slice(0, end);
|
|
89
|
-
return `${head}${head.endsWith('\n') ? '' : '\n'}${
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* @implements A-SPEC-190 (round 7)
|
|
93
|
-
* Ownership is the marker AT ITS PLACE, not the string anywhere in the file. `includes` made any
|
|
94
|
-
* document that merely QUOTES the marker — documentation about holmes-kit, a review note — read as
|
|
95
|
-
* ours: doctor called it `drifted` and the refresh it advised overwrote the user's file with no
|
|
96
|
-
* backup. A quotation is not a claim of ownership.
|
|
97
|
-
*/
|
|
98
|
-
function carriesMarker(content) {
|
|
99
|
-
// Round-8: judging on raw bytes made ownership brittle in the direction that cannot be repaired.
|
|
100
|
-
// A CRLF normalisation (a Windows editor, `core.autocrlf`, `.gitattributes eol=crlf`), a BOM, or a
|
|
101
|
-
// trailing space on the marker line each turned a file the kit itself wrote into `foreign` —
|
|
102
|
-
// permanently, since install then skips it forever and doctor advises deleting it as "yours".
|
|
103
|
-
// Whitespace and line endings are not ownership; the marker is.
|
|
104
|
-
const text = content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
|
105
|
-
const markerAt = (s) => {
|
|
106
|
-
const line = s.split('\n', 1)[0];
|
|
107
|
-
return line.trimEnd() === exports.PLAYBOOK_SKILL_MARKER;
|
|
108
|
-
};
|
|
109
|
-
if (markerAt(text))
|
|
110
|
-
return true; // 옛 배치(마커 먼저) — 우리 것이므로 고칠 수 있다
|
|
111
|
-
const fm = frontmatterEnd(text);
|
|
112
|
-
// 빈 줄 하나가 소유를 지우지 않는다(round-9: frontmatter 뒤 개행 하나로 영구 foreign 이 됐다).
|
|
113
|
-
return fm !== null && markerAt(text.slice(fm).replace(/^\s*\n/, ''));
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* @implements A-SPEC-190 (round 8)
|
|
117
|
-
* Where a leading YAML frontmatter block ends, or null. Round-8 measured the shapes the first
|
|
118
|
-
* regex missed — a BOM, an EMPTY block (`---\n---\n`), and a closing fence at end-of-file with no
|
|
119
|
-
* trailing newline — each of which sent the marker back to byte 0, recreating the CRITICAL the
|
|
120
|
-
* round-7 fix existed to remove while doctor certified the result as `current`.
|
|
121
|
-
*/
|
|
122
|
-
function frontmatterEnd(content) {
|
|
123
|
-
const text = content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
|
124
|
-
if (!/^---[ \t]*(\n|$)/.test(text))
|
|
125
|
-
return null;
|
|
126
|
-
const m = /\n---[ \t]*(\n|$)/.exec(text.slice(3));
|
|
127
|
-
if (!m)
|
|
128
|
-
return null;
|
|
129
|
-
return 3 + m.index + m[0].length;
|
|
85
|
+
return `${head}${head.endsWith('\n') ? '' : '\n'}${installer_markers_1.PLAYBOOK_SKILL_MARKER}\n${text.slice(end)}`;
|
|
130
86
|
}
|
|
131
87
|
/**
|
|
132
88
|
* @implements A-SPEC-172
|
|
@@ -168,7 +124,7 @@ function playbookNames(packageRoot) {
|
|
|
168
124
|
}
|
|
169
125
|
const isOurs = (file) => {
|
|
170
126
|
try {
|
|
171
|
-
return carriesMarker(fs.readFileSync(file, 'utf8'));
|
|
127
|
+
return (0, installer_markers_1.carriesMarker)(fs.readFileSync(file, 'utf8'));
|
|
172
128
|
}
|
|
173
129
|
catch {
|
|
174
130
|
return false;
|
|
@@ -637,7 +593,7 @@ function playbookSkillStates(packageRoot, target) {
|
|
|
637
593
|
out.push({ name, state: err.code === 'ENOENT' && !lstatPresent ? 'missing' : 'foreign' });
|
|
638
594
|
continue;
|
|
639
595
|
}
|
|
640
|
-
const state = !carriesMarker(installed) ? 'foreign'
|
|
596
|
+
const state = !(0, installer_markers_1.carriesMarker)(installed) ? 'foreign'
|
|
641
597
|
: installed === installedSkillBytes(body) ? 'current' : 'drifted';
|
|
642
598
|
out.push({ name, state });
|
|
643
599
|
}
|
|
@@ -33,6 +33,27 @@ export declare function readGrants(root: string): {
|
|
|
33
33
|
grants: Approval[];
|
|
34
34
|
ignored: number;
|
|
35
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* @implements A-SPEC-649
|
|
38
|
+
* A minted grant may carry its BINDING: the workspace it was minted in and the digest of the subject
|
|
39
|
+
* the operator read. Neither is part of the guardrail's `Approval` (an operator's env token has no
|
|
40
|
+
* binding); both are optional so legacy grant files keep covering exactly as before (stated).
|
|
41
|
+
*/
|
|
42
|
+
export type BoundGrant = Approval & {
|
|
43
|
+
workspace?: string;
|
|
44
|
+
subject?: string;
|
|
45
|
+
requestedBy?: string;
|
|
46
|
+
};
|
|
47
|
+
export type BindingRefusal = 'foreign-workspace' | 'stale-subject';
|
|
48
|
+
/** @implements A-SPEC-649 — the grants that WOULD cover this action but are refused by their binding, by name. */
|
|
49
|
+
export declare function refusedGrantReasons(root: string, action: {
|
|
50
|
+
kind: string;
|
|
51
|
+
target: string;
|
|
52
|
+
subject?: string;
|
|
53
|
+
}, now: string): Array<{
|
|
54
|
+
nonce: string;
|
|
55
|
+
reason: BindingRefusal;
|
|
56
|
+
}>;
|
|
36
57
|
/** First grant that covers the action — judged by `approvalCovers` and nothing else. Pure. */
|
|
37
58
|
export declare function pickCoveringGrant(grants: Approval[], action: {
|
|
38
59
|
kind: string;
|
|
@@ -48,6 +69,7 @@ export declare function pickCoveringGrant(grants: Approval[], action: {
|
|
|
48
69
|
export declare function resolveApproval(root: string, envApproval: Approval | undefined, action: {
|
|
49
70
|
kind: string;
|
|
50
71
|
target: string;
|
|
72
|
+
subject?: string;
|
|
51
73
|
}, now: string): {
|
|
52
74
|
approval: Approval;
|
|
53
75
|
source: 'env' | 'grant';
|