@davesheffer/hunch 1.6.0 → 1.7.1
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 +220 -0
- package/bench/constitution-exp03-v1.json +70 -0
- package/dist/cli/index.js +1278 -44
- package/dist/constitution/adapters.js +487 -0
- package/dist/constitution/behaviorAttestationBinding.js +17 -0
- package/dist/constitution/behaviorEvaluator.js +220 -0
- package/dist/constitution/behaviorProof.js +205 -0
- package/dist/constitution/behaviorWorkspace.js +124 -0
- package/dist/constitution/bootstrap.js +133 -0
- package/dist/constitution/canonical.js +51 -0
- package/dist/constitution/card.js +133 -0
- package/dist/constitution/compiler.js +176 -0
- package/dist/constitution/composition.js +101 -0
- package/dist/constitution/corpus.js +58 -0
- package/dist/constitution/delta.js +154 -0
- package/dist/constitution/disposition.js +141 -0
- package/dist/constitution/evaluator.js +435 -0
- package/dist/constitution/experiment.js +948 -0
- package/dist/constitution/experimentRunner.js +344 -0
- package/dist/constitution/g2.js +291 -0
- package/dist/constitution/g2BehaviorAttestation.js +209 -0
- package/dist/constitution/g2BehaviorCandidates.js +703 -0
- package/dist/constitution/g2BehaviorDependencies.js +379 -0
- package/dist/constitution/g2BehaviorMaterialization.js +171 -0
- package/dist/constitution/g2BehaviorPolicyMaterializer.js +241 -0
- package/dist/constitution/g2CandidateAttestation.js +179 -0
- package/dist/constitution/g2Candidates.js +195 -0
- package/dist/constitution/g2Drills.js +122 -0
- package/dist/constitution/g3.js +511 -0
- package/dist/constitution/g3Conformance.js +115 -0
- package/dist/constitution/lifecycle.js +189 -0
- package/dist/constitution/mutation.js +262 -0
- package/dist/constitution/nodeTestEvidence.js +47 -0
- package/dist/constitution/plan.js +172 -0
- package/dist/constitution/policyRuntime.js +8 -0
- package/dist/constitution/proof.js +166 -0
- package/dist/constitution/replay.js +361 -0
- package/dist/constitution/replayCache.js +89 -0
- package/dist/constitution/replayWorker.js +34 -0
- package/dist/constitution/repository.js +533 -0
- package/dist/constitution/schema.js +545 -0
- package/dist/constitution/scorecard.js +106 -0
- package/dist/constitution/service.js +1149 -0
- package/dist/constitution/shadow.js +235 -0
- package/dist/constitution/sourceMutation.js +316 -0
- package/dist/constitution/structural.js +601 -0
- package/dist/core/autoreview.js +27 -3
- package/dist/core/dupdetect.js +10 -3
- package/dist/core/events.js +61 -0
- package/dist/core/externalImports.js +24 -0
- package/dist/core/hookpolicy.js +3 -0
- package/dist/core/relativeImports.js +33 -0
- package/dist/core/stats.js +115 -0
- package/dist/extractors/git.js +81 -0
- package/dist/extractors/indexer.js +39 -38
- package/dist/extractors/nativeTreeSitter.js +108 -0
- package/dist/extractors/parse.js +5 -15
- package/dist/integrations/claudemd.js +8 -1
- package/dist/integrations/gitignore.js +8 -0
- package/dist/integrations/providers.js +32 -10
- package/dist/integrations/sync.js +16 -1
- package/dist/mcp/server.js +284 -0
- package/dist/synthesis/provider.js +145 -37
- package/dist/synthesis/synthesize.js +4 -4
- package/package.json +5 -1
package/dist/cli/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import "./preflight.js"; // MUST stay the first import — Node-version gate bef
|
|
|
17
17
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, realpathSync } from "node:fs";
|
|
18
18
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
19
19
|
import { join, relative, dirname, basename, resolve, isAbsolute } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
20
21
|
import { Command } from "commander";
|
|
21
22
|
import { hunchPaths, hunchPathsForDir, findRoot, toPosixTarget } from "../core/paths.js";
|
|
22
23
|
import { writeFileAtomic } from "../core/io.js";
|
|
@@ -28,8 +29,8 @@ import { selectEmbedder } from "../store/embedder.js";
|
|
|
28
29
|
import { indexRepo } from "../extractors/indexer.js";
|
|
29
30
|
import { syncCommit, recordFailure, captureTestRun } from "../synthesis/synthesize.js";
|
|
30
31
|
import { parseTestReport } from "../extractors/testreport.js";
|
|
31
|
-
import { selectProvider } from "../synthesis/provider.js";
|
|
32
|
-
import { isGitRepo, headSha, logSince, lastChangeDate, stagedFiles, workingFiles, commitFiles, asOfDate, stagedDiff, workingDiff, commitDiff, rangeFiles, rangeDiff, rangeSubjects, revExists, commitAndPushHunch, pullHunch, gitUntrackCached, gitCommonDir, isLinkedWorktree, mainWorktreeRoot } from "../extractors/git.js";
|
|
32
|
+
import { readSynthesisPreference, resolveSynthesisProvider, selectProvider, SYNTH_PREFERENCES, writeSynthesisPreference, } from "../synthesis/provider.js";
|
|
33
|
+
import { isGitRepo, headSha, logSince, lastChangeDate, stagedFiles, workingFiles, commitFiles, asOfDate, stagedDiff, workingDiff, commitDiff, rangeFiles, rangeDiff, rangeSubjects, revExists, revParse, commitAndPushHunch, pullHunch, gitUntrackCached, gitCommonDir, isLinkedWorktree, mainWorktreeRoot } from "../extractors/git.js";
|
|
33
34
|
import { writeTeamConfig, ensureTeamOverlay, readTeamConfig } from "../integrations/team.js";
|
|
34
35
|
import { runbookId, decisionId } from "../core/ids.js";
|
|
35
36
|
import { deriveForbids, effectiveForbids } from "../core/constraintmatch.js";
|
|
@@ -44,25 +45,34 @@ import { ensureGitignore, ignoreHunchMemory, HUNCH_MEMORY_DIRS } from "../integr
|
|
|
44
45
|
import { writeCiWorkflow } from "../integrations/ciAction.js";
|
|
45
46
|
import { updateClaudeMd } from "../integrations/claudemd.js";
|
|
46
47
|
import { writeMcpJson, writeSlashCommands, installClaudeHooks } from "../integrations/scaffold.js";
|
|
47
|
-
import { scaffoldProviders, regenerateGrounding, refreshExistingGrounding } from "../integrations/providers.js";
|
|
48
|
+
import { scaffoldProviders, regenerateGrounding, refreshExistingGrounding, refreshCommittableGrounding } from "../integrations/providers.js";
|
|
48
49
|
import { healClaudeConfigCaseSplit } from "../integrations/claudeConfig.js";
|
|
49
50
|
import { formatContext, formatStructure } from "../core/format.js";
|
|
50
51
|
import { readConfig, writeConfig, FIRMNESS_LEVELS, isFirmness } from "../core/config.js";
|
|
51
52
|
import { blockingInScope, vetoInScope, proposedEditLines } from "../core/hookpolicy.js";
|
|
53
|
+
import { appendEvent, readEvents } from "../core/events.js";
|
|
54
|
+
import { computeStats, formatStats } from "../core/stats.js";
|
|
52
55
|
import { injectionMode } from "../core/hookcache.js";
|
|
53
56
|
import { contextHookOutput, denyHookOutput, hookProvider, normalizeHookEvent, stopHookOutput } from "../core/agenthook.js";
|
|
54
57
|
import { PIPELINE_LOOP, UNVERIFIED_NAG, loadPipelineState, onCommand, onEdit, onPrompt, onSkill, pipelineEnabled, savePipelineState, stopVerdict, } from "../core/pipeline.js";
|
|
55
|
-
import { draftDuplicateOf } from "../core/dupdetect.js";
|
|
58
|
+
import { draftDuplicateOf, isAcceptedDuplicateAnchor } from "../core/dupdetect.js";
|
|
56
59
|
import { planAutoReview, planMutations } from "../core/autoreview.js";
|
|
57
60
|
import { loadGoldenSet, evaluateGraphLift } from "../eval/harness.js";
|
|
58
61
|
import { loadGuardCases, evalGuards, generateGuardCases } from "../eval/guards.js";
|
|
59
62
|
import { computeDrift } from "../core/drift.js";
|
|
63
|
+
import { renderCompilerScorecard, scoreCompilerCaseBank } from "../constitution/scorecard.js";
|
|
60
64
|
import { generateWiki, wikiStatus, wikiPrompt, publicHome, privateHome, readWikiManifestAt, nowData } from "../wiki/wiki.js";
|
|
61
65
|
import { adoptProsePrompt } from "../wiki/adopt.js";
|
|
62
66
|
import { topicCollisions, renderGrounding } from "../core/topics.js";
|
|
63
67
|
import { parseDocAnchors, renderDocGrounding } from "../core/docanchors.js";
|
|
64
68
|
import { compareCandidates } from "../core/compare.js";
|
|
65
69
|
import { checkConformance } from "../core/conformance.js";
|
|
70
|
+
import { ConstitutionService } from "../constitution/service.js";
|
|
71
|
+
import { renderProofCard } from "../constitution/card.js";
|
|
72
|
+
import { movePolicyArtifactsToPrivate } from "../constitution/repository.js";
|
|
73
|
+
import { HistoryDispositionClassificationSchema } from "../constitution/schema.js";
|
|
74
|
+
import { G2_RUNBOOK_CATEGORIES } from "../constitution/g2.js";
|
|
75
|
+
import { subscriptionCliVersion } from "../constitution/experimentRunner.js";
|
|
66
76
|
import { draftTripwires, knownRepoDeps } from "../synthesis/tripwires.js";
|
|
67
77
|
import { constraintId } from "../core/ids.js";
|
|
68
78
|
import { readManifest, writeManifest, SCHEMA_VERSION } from "../core/migrate.js";
|
|
@@ -333,15 +343,20 @@ program
|
|
|
333
343
|
verify: opts.verify,
|
|
334
344
|
samples: parseSamples(opts.samples),
|
|
335
345
|
});
|
|
346
|
+
const doCommit = opts.commit ?? store.autoCommit;
|
|
336
347
|
if (r.status === "written") {
|
|
337
348
|
store.reindex();
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
349
|
+
// Refresh grounding so committed counts track the store. Git-CLEAN docs are
|
|
350
|
+
// refreshed and folded into the capture commit below (kills the refresh-counts
|
|
351
|
+
// treadmill: every capture bumped the count and re-staled the docs for the
|
|
352
|
+
// release gate's clean-tree check). A user-dirty doc is never touched from the
|
|
353
|
+
// hook; manual `hunch sync` still self-heals ALL existing grounding docs.
|
|
354
|
+
const groundingToStage = toOverlay ? [] : refreshCommittableGrounding(root, store);
|
|
341
355
|
if (!opts.fromHook) {
|
|
342
356
|
const healed = refreshExistingGrounding(root, store);
|
|
343
|
-
|
|
344
|
-
|
|
357
|
+
const refreshed = [...new Set([...groundingToStage.map((file) => relative(root, file)), ...healed])];
|
|
358
|
+
if (refreshed.length && !opts.quiet)
|
|
359
|
+
console.log(` ↳ grounding refreshed: ${refreshed.join(", ")}`);
|
|
345
360
|
}
|
|
346
361
|
// Persist the captured decision in the repo it landed in (private store under
|
|
347
362
|
// --private, else this repo). ON by default (follows auto-commit; --no-commit or
|
|
@@ -351,10 +366,9 @@ program
|
|
|
351
366
|
// hook (no recursion). The overlay is pushed; the public .hunch/ is committed
|
|
352
367
|
// WITHOUT pushing — auto-pushing the user's code branch would publish their
|
|
353
368
|
// unpushed commits (bug_overlay_clobber lineage).
|
|
354
|
-
const doCommit = opts.commit ?? store.autoCommit;
|
|
355
369
|
const commitTarget = doCommit ? (toOverlay ? store.privateDir : hunchPaths(root).hunch) : undefined;
|
|
356
370
|
if (commitTarget) {
|
|
357
|
-
commitAndPushHunch(commitTarget, `hunch: capture ${r.decision?.id ?? "decision"}`, { push: toOverlay });
|
|
371
|
+
commitAndPushHunch(commitTarget, `hunch: capture ${r.decision?.id ?? "decision"}`, { push: toOverlay, alsoStage: groundingToStage });
|
|
358
372
|
if (!opts.quiet)
|
|
359
373
|
console.log(` ↳ committed ${toOverlay ? "+ pushed " : ""}${r.decision?.id} (${commitTarget}${toOverlay ? "" : " — rides your next push"})`);
|
|
360
374
|
}
|
|
@@ -364,6 +378,26 @@ program
|
|
|
364
378
|
else if (!opts.quiet) {
|
|
365
379
|
console.log(`· skipped: ${r.reason}`);
|
|
366
380
|
}
|
|
381
|
+
try {
|
|
382
|
+
const constitution = new ConstitutionService(store, root);
|
|
383
|
+
if (constitution.g2Repository.currentPlan()) {
|
|
384
|
+
indexRepo(store, root, { churn: false });
|
|
385
|
+
store.reindex();
|
|
386
|
+
const sweep = constitution.g2ShadowSweep();
|
|
387
|
+
if (sweep.recorded.length && doCommit && store.privateDir) {
|
|
388
|
+
commitAndPushHunch(store.privateDir, `hunch: record ${sweep.recorded.length} G2 shadow observation(s)`);
|
|
389
|
+
}
|
|
390
|
+
if (!opts.quiet) {
|
|
391
|
+
console.log(` ↳ G2 shadow: ${sweep.recorded.length} recorded · ${sweep.existing.length} existing · ${sweep.failures.length} failed; authority none`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch (e) {
|
|
396
|
+
// Post-commit learning is background/best-effort. Shadow operation must
|
|
397
|
+
// never make a source commit, decision capture, warning, or block fail.
|
|
398
|
+
if (!opts.quiet)
|
|
399
|
+
console.log(` ↳ G2 shadow skipped safely: ${e.message}`);
|
|
400
|
+
}
|
|
367
401
|
store.close();
|
|
368
402
|
});
|
|
369
403
|
function configureOverlay(dir, opts, mode) {
|
|
@@ -445,7 +479,20 @@ function configureOverlay(dir, opts, mode) {
|
|
|
445
479
|
// a store elsewhere on disk. Resolution (env || local.json) re-resolves against root.
|
|
446
480
|
const rel = relative(root, hunchDir);
|
|
447
481
|
const stored = rel && !rel.startsWith("..") && !isAbsolute(rel) ? toPosixTarget(rel) : hunchDir;
|
|
448
|
-
|
|
482
|
+
const localFile = join(paths.hunch, "local.json");
|
|
483
|
+
let existingLocal = {};
|
|
484
|
+
if (existsSync(localFile)) {
|
|
485
|
+
try {
|
|
486
|
+
const parsed = JSON.parse(readFileSync(localFile, "utf8"));
|
|
487
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object")
|
|
488
|
+
throw new Error("not an object");
|
|
489
|
+
existingLocal = parsed;
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
return fail(`refusing to overwrite malformed local configuration: ${localFile}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
writeFileAtomic(localFile, JSON.stringify({ ...existingLocal, privateDir: stored, autoCommit: !!opts.autoCommit, mode }, null, 2) + "\n");
|
|
449
496
|
ensureGitignore(root); // keeps .hunch/local.json + .hunch-private/ out of git
|
|
450
497
|
// SHARED mode with a remote: publish the store's URL in a COMMITTED team.json, so a
|
|
451
498
|
// fresh clone / new teammate / headless agent auto-connects on `hunch init` (or MCP
|
|
@@ -478,6 +525,7 @@ function configureOverlay(dir, opts, mode) {
|
|
|
478
525
|
const pub = new JsonStore(paths);
|
|
479
526
|
const priv = new JsonStore(hunchPathsForDir(hunchDir));
|
|
480
527
|
const res = movePublicMemoryToPrivate(pub, priv);
|
|
528
|
+
const constitutionMoved = movePolicyArtifactsToPrivate(paths.hunch, hunchDir);
|
|
481
529
|
for (const kind of ENTITY_KINDS)
|
|
482
530
|
pub.dropAll(kind); // public store now empty on disk
|
|
483
531
|
if (isGitRepo(root))
|
|
@@ -487,7 +535,22 @@ function configureOverlay(dir, opts, mode) {
|
|
|
487
535
|
const grounding = regenerateGrounding(root, gstore);
|
|
488
536
|
gstore.close();
|
|
489
537
|
commitAndPushHunch(hunchDir, "hunch: absorb public memory into private overlay"); // durable
|
|
490
|
-
const
|
|
538
|
+
const breakdownParts = Object.entries(res.moved).map(([k, n]) => `${n} ${k}`);
|
|
539
|
+
if (constitutionMoved.policies)
|
|
540
|
+
breakdownParts.push(`${constitutionMoved.policies} policies`);
|
|
541
|
+
if (constitutionMoved.proofs)
|
|
542
|
+
breakdownParts.push(`${constitutionMoved.proofs} proofs`);
|
|
543
|
+
if (constitutionMoved.plans)
|
|
544
|
+
breakdownParts.push(`${constitutionMoved.plans} proof plans`);
|
|
545
|
+
if (constitutionMoved.evidence)
|
|
546
|
+
breakdownParts.push(`${constitutionMoved.evidence} evidence events`);
|
|
547
|
+
if (constitutionMoved.corpora)
|
|
548
|
+
breakdownParts.push(`${constitutionMoved.corpora} proof corpora`);
|
|
549
|
+
if (constitutionMoved.dispositions)
|
|
550
|
+
breakdownParts.push(`${constitutionMoved.dispositions} history dispositions`);
|
|
551
|
+
if (constitutionMoved.shadow)
|
|
552
|
+
breakdownParts.push(`${constitutionMoved.shadow} shadow records`);
|
|
553
|
+
const breakdown = breakdownParts.join(", ") || "0 records";
|
|
491
554
|
migrateNote =
|
|
492
555
|
` ✓ migrated public memory → overlay (${breakdown}); public store emptied\n` +
|
|
493
556
|
` ✓ untracked + gitignored the .hunch memory tree — this repo is now CODE-ONLY\n` +
|
|
@@ -922,6 +985,1007 @@ program
|
|
|
922
985
|
}
|
|
923
986
|
store.close();
|
|
924
987
|
});
|
|
988
|
+
// ---- policy (Hunch Constitution — versioned policy/proof lifecycle) -------
|
|
989
|
+
const policyCmd = program
|
|
990
|
+
.command("policy")
|
|
991
|
+
.description("Hunch Constitution: compile, prove, inspect, activate, and evaluate deterministic engineering policy.");
|
|
992
|
+
policyCmd
|
|
993
|
+
.command("list")
|
|
994
|
+
.description("List Policy IR records from the public store plus the local private overlay.")
|
|
995
|
+
.option("--state <state>", "filter by lifecycle state")
|
|
996
|
+
.option("--public-only", "exclude private-overlay policy records")
|
|
997
|
+
.action((opts) => {
|
|
998
|
+
const { store, root } = storeFor();
|
|
999
|
+
try {
|
|
1000
|
+
const policies = new ConstitutionService(store, root).list({ state: opts.state, publicOnly: opts.publicOnly });
|
|
1001
|
+
if (!policies.length) {
|
|
1002
|
+
console.log("No Constitution policies found.");
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
for (const p of policies) {
|
|
1006
|
+
console.log(`${p.id} [${p.state}] [${p.severity}] ${p.statement}${p.data_class === "public" ? "" : ` [${p.data_class}]`}`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
catch (e) {
|
|
1010
|
+
fail(e.message);
|
|
1011
|
+
}
|
|
1012
|
+
finally {
|
|
1013
|
+
store.close();
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
policyCmd
|
|
1017
|
+
.command("show")
|
|
1018
|
+
.description("Show canonical Policy IR and, optionally, its proof artifact.")
|
|
1019
|
+
.argument("<id>", "policy id")
|
|
1020
|
+
.option("--proof", "include the linked proof artifact")
|
|
1021
|
+
.option("--public-only", "exclude private-overlay policy records")
|
|
1022
|
+
.action((id, opts) => {
|
|
1023
|
+
const { store, root } = storeFor();
|
|
1024
|
+
try {
|
|
1025
|
+
const service = new ConstitutionService(store, root);
|
|
1026
|
+
const policy = service.get(id, { publicOnly: opts.publicOnly });
|
|
1027
|
+
const output = { policy };
|
|
1028
|
+
if (opts.proof && policy.proof)
|
|
1029
|
+
output.proof = service.proof(policy.proof, { publicOnly: opts.publicOnly });
|
|
1030
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1031
|
+
}
|
|
1032
|
+
catch (e) {
|
|
1033
|
+
fail(e.message);
|
|
1034
|
+
}
|
|
1035
|
+
finally {
|
|
1036
|
+
store.close();
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
policyCmd
|
|
1040
|
+
.command("corpus")
|
|
1041
|
+
.description("Import or inspect a policy-bound known-good/known-bad commit corpus. Known-good fixtures may carry a human attestation; imported refs resolve to immutable SHAs.")
|
|
1042
|
+
.argument("<id>", "policy id")
|
|
1043
|
+
.option("--import <file>", "JSON file with known_bad/known_good ref and label arrays; known_good may include attestation { actor, reason }")
|
|
1044
|
+
.option("--public-only", "exclude private-overlay policy/corpus records when inspecting")
|
|
1045
|
+
.action((id, opts) => {
|
|
1046
|
+
const { store, root } = storeFor();
|
|
1047
|
+
try {
|
|
1048
|
+
const service = new ConstitutionService(store, root);
|
|
1049
|
+
if (opts.import) {
|
|
1050
|
+
if (opts.publicOnly)
|
|
1051
|
+
throw new Error("--public-only cannot be combined with --import");
|
|
1052
|
+
const file = resolve(root, opts.import);
|
|
1053
|
+
const corpus = service.importCorpus(id, JSON.parse(readFileSync(file, "utf8")));
|
|
1054
|
+
const attestedGood = corpus.known_good.filter((fixture) => !!fixture.attestation).length;
|
|
1055
|
+
console.log(`✓ imported ${corpus.id} for ${corpus.policy_id}: ${corpus.known_bad.length} known bad, ${corpus.known_good.length} known good (${attestedGood} human-attested)`);
|
|
1056
|
+
console.log(` hash: ${corpus.content_hash} · home follows policy data class (${corpus.data_class})`);
|
|
1057
|
+
}
|
|
1058
|
+
else {
|
|
1059
|
+
console.log(JSON.stringify(service.corpus(id, { publicOnly: opts.publicOnly }), null, 2));
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
catch (e) {
|
|
1063
|
+
fail(e.message);
|
|
1064
|
+
}
|
|
1065
|
+
finally {
|
|
1066
|
+
store.close();
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
policyCmd
|
|
1070
|
+
.command("card")
|
|
1071
|
+
.description("Render the deterministic proof card: exact policy, evidence vector, uncertainty, authority, and next actions.")
|
|
1072
|
+
.argument("<id>", "policy id")
|
|
1073
|
+
.option("--json", "emit the canonical proof-card object")
|
|
1074
|
+
.option("--public-only", "exclude private-overlay policy/proof records")
|
|
1075
|
+
.action((id, opts) => {
|
|
1076
|
+
const { store, root } = storeFor();
|
|
1077
|
+
try {
|
|
1078
|
+
const card = new ConstitutionService(store, root).card(id, { publicOnly: opts.publicOnly });
|
|
1079
|
+
console.log(opts.json ? JSON.stringify(card, null, 2) : renderProofCard(card));
|
|
1080
|
+
}
|
|
1081
|
+
catch (e) {
|
|
1082
|
+
fail(e.message);
|
|
1083
|
+
}
|
|
1084
|
+
finally {
|
|
1085
|
+
store.close();
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
policyCmd
|
|
1089
|
+
.command("compile")
|
|
1090
|
+
.description("Compile one structured Decision.conformance predicate into a non-active Policy IR candidate.")
|
|
1091
|
+
.argument("<decision-id>", "source decision id")
|
|
1092
|
+
.option("--through <selector>", "compile a negative boundary as must-pass-through via this symbol selector")
|
|
1093
|
+
.option("--private", "write the policy/proof only to the configured private overlay")
|
|
1094
|
+
.action((decisionId, opts) => {
|
|
1095
|
+
const { store, root } = storeFor();
|
|
1096
|
+
try {
|
|
1097
|
+
const policy = new ConstitutionService(store, root).compile(decisionId, opts);
|
|
1098
|
+
console.log(`✓ compiled ${policy.id} [${policy.state}] — ${policy.statement}`);
|
|
1099
|
+
console.log(` ${policy.assertion.kind}: ${JSON.stringify(policy.assertion)}`);
|
|
1100
|
+
console.log(" authority: none; this candidate cannot block until proved and explicitly accepted by a human.");
|
|
1101
|
+
}
|
|
1102
|
+
catch (e) {
|
|
1103
|
+
fail(e.message);
|
|
1104
|
+
}
|
|
1105
|
+
finally {
|
|
1106
|
+
store.close();
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
policyCmd
|
|
1110
|
+
.command("plan")
|
|
1111
|
+
.description("Generate or inspect the canonical, non-executing ProofPlan for a Policy IR candidate.")
|
|
1112
|
+
.argument("<id>", "policy id")
|
|
1113
|
+
.option("--history <n>", "maximum accepted-history commits", "20")
|
|
1114
|
+
.option("--mutations <n>", "maximum deterministic mutations", "3")
|
|
1115
|
+
.option("--minutes <n>", "total future replay budget in minutes", "5")
|
|
1116
|
+
.option("--public-only", "exclude private-overlay policy/evidence records")
|
|
1117
|
+
.action((id, opts) => {
|
|
1118
|
+
const { store, root } = storeFor();
|
|
1119
|
+
try {
|
|
1120
|
+
const values = [opts.history, opts.mutations, opts.minutes].map(Number);
|
|
1121
|
+
if (values.slice(0, 2).some((n) => !Number.isFinite(n) || n < 0) || !Number.isFinite(values[2]) || values[2] <= 0) {
|
|
1122
|
+
throw new Error("history/mutation budgets must be non-negative and minutes must be positive");
|
|
1123
|
+
}
|
|
1124
|
+
const plan = new ConstitutionService(store, root).plan(id, {
|
|
1125
|
+
maxCommits: values[0],
|
|
1126
|
+
maxMutations: values[1],
|
|
1127
|
+
maxMinutes: values[2],
|
|
1128
|
+
publicOnly: opts.publicOnly,
|
|
1129
|
+
});
|
|
1130
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
1131
|
+
}
|
|
1132
|
+
catch (e) {
|
|
1133
|
+
fail(e.message);
|
|
1134
|
+
}
|
|
1135
|
+
finally {
|
|
1136
|
+
store.close();
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
policyCmd
|
|
1140
|
+
.command("prove")
|
|
1141
|
+
.description("Execute the canonical ProofPlan: isolated current/history/control replay plus deterministic mutation; grants no authority.")
|
|
1142
|
+
.argument("<id>", "policy id")
|
|
1143
|
+
.action((id) => {
|
|
1144
|
+
const { store, root } = storeFor();
|
|
1145
|
+
try {
|
|
1146
|
+
indexRepo(store, root, { churn: false });
|
|
1147
|
+
store.reindex();
|
|
1148
|
+
const { policy, proof } = new ConstitutionService(store, root).prove(id);
|
|
1149
|
+
console.log(`POLICY PROOF ${policy.id}`);
|
|
1150
|
+
console.log(` state: ${policy.state} · class: ${proof.proof_class} · proof: ${proof.id}`);
|
|
1151
|
+
console.log(` current: ${proof.current.satisfied} satisfied · ${proof.current.violated} violated · ${proof.current.unknown} unknown · ${proof.current.error} error`);
|
|
1152
|
+
console.log(` known bad: ${proof.known_bad.violated}/${proof.known_bad.total} caught · known good: ${proof.known_good.satisfied}/${proof.known_good.total} satisfied`);
|
|
1153
|
+
console.log(` accepted history: ${proof.accepted_history.total} evaluated · ${proof.accepted_history.violated} unclassified hit · ${proof.accepted_history.unknown} unknown · ${proof.accepted_history.error} error`);
|
|
1154
|
+
console.log(` mutations: ${proof.mutations.violated}/${proof.mutations.total} caught · ${Object.keys(proof.mutations.operator_coverage).join(", ") || "none"}`);
|
|
1155
|
+
for (const limitation of proof.limitations)
|
|
1156
|
+
console.log(` limitation: ${limitation}`);
|
|
1157
|
+
console.log(" next: hunch policy accept " + policy.id + " --advisory|--blocking --actor human:<identity>");
|
|
1158
|
+
}
|
|
1159
|
+
catch (e) {
|
|
1160
|
+
fail(e.message);
|
|
1161
|
+
}
|
|
1162
|
+
finally {
|
|
1163
|
+
store.close();
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
policyCmd
|
|
1167
|
+
.command("history")
|
|
1168
|
+
.description("Inspect or human-classify exact violated accepted-history receipts. Classifications are append-only and grant no activation authority.")
|
|
1169
|
+
.argument("<id>", "policy id")
|
|
1170
|
+
.option("--commit <sha>", "full 40-character accepted-history commit SHA")
|
|
1171
|
+
.option("--classify <kind>", "true_positive_actionable | true_positive_accepted_exception | false_positive_selector | false_positive_semantics | false_positive_stale | unknown_insufficient_parser")
|
|
1172
|
+
.option("--actor <identity>", "explicit human identity: human:, github:, or git:")
|
|
1173
|
+
.option("--reason <reason>", "bounded audited reason for the classification")
|
|
1174
|
+
.option("--supersedes <id>", "current disposition id when appending a corrected classification")
|
|
1175
|
+
.option("--public-only", "exclude private-overlay policy/disposition records when inspecting")
|
|
1176
|
+
.action((id, opts) => {
|
|
1177
|
+
const { store, root } = storeFor();
|
|
1178
|
+
try {
|
|
1179
|
+
const service = new ConstitutionService(store, root);
|
|
1180
|
+
const writing = !!opts.commit || !!opts.classify || !!opts.actor || !!opts.reason || !!opts.supersedes;
|
|
1181
|
+
if (writing) {
|
|
1182
|
+
if (opts.publicOnly)
|
|
1183
|
+
throw new Error("--public-only cannot be combined with history classification");
|
|
1184
|
+
if (!opts.commit || !opts.classify || !opts.actor || !opts.reason) {
|
|
1185
|
+
throw new Error("history classification requires --commit, --classify, --actor, and --reason");
|
|
1186
|
+
}
|
|
1187
|
+
const classification = HistoryDispositionClassificationSchema.parse(opts.classify);
|
|
1188
|
+
const disposition = service.classifyHistory(id, opts.commit, classification, opts.actor, opts.reason, { supersedes: opts.supersedes });
|
|
1189
|
+
console.log(`✓ recorded ${disposition.id}: ${disposition.classification} for ${disposition.commit}`);
|
|
1190
|
+
console.log(` proof: ${disposition.proof_id} · actor: ${disposition.actor} · home follows policy data class (${disposition.data_class})`);
|
|
1191
|
+
console.log(" classification grants no activation authority; blocking still requires an explicit human policy acceptance.");
|
|
1192
|
+
}
|
|
1193
|
+
else {
|
|
1194
|
+
console.log(JSON.stringify(service.historyDispositions(id, { publicOnly: opts.publicOnly }), null, 2));
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
catch (e) {
|
|
1198
|
+
fail(e.message);
|
|
1199
|
+
}
|
|
1200
|
+
finally {
|
|
1201
|
+
store.close();
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
policyCmd
|
|
1205
|
+
.command("shadow")
|
|
1206
|
+
.description("Record or inspect non-blocking shadow evaluations, append human dispositions, and derive raw precision/P4-readiness measurements. Never activates policy.")
|
|
1207
|
+
.argument("<id>", "policy id")
|
|
1208
|
+
.option("--record", "record the current deterministic evaluation once for this exact graph receipt")
|
|
1209
|
+
.option("--event <id>", "shadow evaluation id to classify")
|
|
1210
|
+
.option("--classify <kind>", "true_positive_actionable | true_positive_accepted_exception | false_positive_selector | false_positive_semantics | false_positive_stale | unknown_insufficient_parser")
|
|
1211
|
+
.option("--actor <identity>", "explicit human identity: human:, github:, or git:")
|
|
1212
|
+
.option("--reason <reason>", "bounded audited reason for the classification")
|
|
1213
|
+
.option("--supersedes <id>", "current shadow disposition id when appending a correction")
|
|
1214
|
+
.option("--min-applicable <n>", "minimum recent applicable changes for P4 review eligibility", "20")
|
|
1215
|
+
.option("--recent <n>", "maximum recent applicable changes in the precision window", "100")
|
|
1216
|
+
.option("--max-unknown-error-rate <rate>", "exclusive unknown/error-rate ceiling", "0.01")
|
|
1217
|
+
.option("--public-only", "exclude private-overlay shadow records when inspecting")
|
|
1218
|
+
.action((id, opts) => {
|
|
1219
|
+
const { store, root } = storeFor();
|
|
1220
|
+
try {
|
|
1221
|
+
const service = new ConstitutionService(store, root);
|
|
1222
|
+
const classifying = !!opts.event || !!opts.classify || !!opts.actor || !!opts.reason || !!opts.supersedes;
|
|
1223
|
+
if (opts.record && classifying)
|
|
1224
|
+
throw new Error("choose either --record or a shadow classification, not both");
|
|
1225
|
+
if ((opts.record || classifying) && opts.publicOnly)
|
|
1226
|
+
throw new Error("--public-only cannot be combined with shadow writes");
|
|
1227
|
+
if (opts.record) {
|
|
1228
|
+
indexRepo(store, root, { churn: false });
|
|
1229
|
+
store.reindex();
|
|
1230
|
+
const record = service.recordShadow(id);
|
|
1231
|
+
console.log(`✓ recorded ${record.id}: ${record.evaluation.result} on ${record.evaluation.repository.graph_hash}`);
|
|
1232
|
+
console.log(" shadow recording never warns, blocks, changes lifecycle, or grants authority.");
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
if (classifying) {
|
|
1236
|
+
if (!opts.event || !opts.classify || !opts.actor || !opts.reason) {
|
|
1237
|
+
throw new Error("shadow classification requires --event, --classify, --actor, and --reason");
|
|
1238
|
+
}
|
|
1239
|
+
const classification = HistoryDispositionClassificationSchema.parse(opts.classify);
|
|
1240
|
+
const disposition = service.classifyShadow(id, opts.event, classification, opts.actor, opts.reason, { supersedes: opts.supersedes });
|
|
1241
|
+
console.log(`✓ recorded ${disposition.id}: ${disposition.classification} for ${disposition.shadow_id}`);
|
|
1242
|
+
console.log(" disposition changes measurement only; it cannot activate or block.");
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
const minApplicable = Number(opts.minApplicable);
|
|
1246
|
+
const recentApplicable = Number(opts.recent);
|
|
1247
|
+
const maxUnknownErrorRate = Number(opts.maxUnknownErrorRate);
|
|
1248
|
+
console.log(JSON.stringify(service.shadowReport(id, { minApplicable, recentApplicable, maxUnknownErrorRate }, { publicOnly: opts.publicOnly }), null, 2));
|
|
1249
|
+
}
|
|
1250
|
+
catch (e) {
|
|
1251
|
+
fail(e.message);
|
|
1252
|
+
}
|
|
1253
|
+
finally {
|
|
1254
|
+
store.close();
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
policyCmd
|
|
1258
|
+
.command("accept")
|
|
1259
|
+
.description("Human-only activation of a proved policy. Blocking requires P3+ proof.")
|
|
1260
|
+
.argument("<id>", "policy id")
|
|
1261
|
+
.option("--advisory", "activate as advisory")
|
|
1262
|
+
.option("--blocking", "activate as blocking")
|
|
1263
|
+
.requiredOption("--actor <identity>", "explicit human identity: human:, github:, or git:")
|
|
1264
|
+
.action((id, opts) => {
|
|
1265
|
+
const { store, root } = storeFor();
|
|
1266
|
+
try {
|
|
1267
|
+
if (!!opts.advisory === !!opts.blocking)
|
|
1268
|
+
throw new Error("choose exactly one of --advisory or --blocking");
|
|
1269
|
+
const mode = opts.blocking ? "blocking" : "advisory";
|
|
1270
|
+
const policy = new ConstitutionService(store, root).approve(id, mode, opts.actor);
|
|
1271
|
+
console.log(`✓ ${policy.id} is ${policy.state} by ${policy.authority?.actor} (revision ${policy.revision})`);
|
|
1272
|
+
}
|
|
1273
|
+
catch (e) {
|
|
1274
|
+
fail(e.message);
|
|
1275
|
+
}
|
|
1276
|
+
finally {
|
|
1277
|
+
store.close();
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
policyCmd
|
|
1281
|
+
.command("demote")
|
|
1282
|
+
.description("Immediately demote an active blocking policy to advisory without erasing history.")
|
|
1283
|
+
.argument("<id>", "policy id")
|
|
1284
|
+
.requiredOption("--actor <identity>", "explicit human identity: human:, github:, or git:")
|
|
1285
|
+
.requiredOption("--reason <reason>", "audited demotion reason")
|
|
1286
|
+
.action((id, opts) => {
|
|
1287
|
+
const { store, root } = storeFor();
|
|
1288
|
+
try {
|
|
1289
|
+
const policy = new ConstitutionService(store, root).demote(id, opts.actor, opts.reason);
|
|
1290
|
+
console.log(`✓ ${policy.id} demoted to ${policy.state}; history retained (revision ${policy.revision})`);
|
|
1291
|
+
}
|
|
1292
|
+
catch (e) {
|
|
1293
|
+
fail(e.message);
|
|
1294
|
+
}
|
|
1295
|
+
finally {
|
|
1296
|
+
store.close();
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
policyCmd
|
|
1300
|
+
.command("exception")
|
|
1301
|
+
.description("Human-link a strictly narrower opposite policy to its parent; invalidates child proof/authority and never enables blocking.")
|
|
1302
|
+
.argument("<id>", "narrow exception policy id")
|
|
1303
|
+
.requiredOption("--parent <id>", "broader parent policy id")
|
|
1304
|
+
.requiredOption("--actor <identity>", "explicit human identity: human:, github:, or git:")
|
|
1305
|
+
.requiredOption("--reason <reason>", "audited reason this narrow opposite is intentional")
|
|
1306
|
+
.action((id, opts) => {
|
|
1307
|
+
const { store, root } = storeFor();
|
|
1308
|
+
try {
|
|
1309
|
+
const policy = new ConstitutionService(store, root).linkException(id, opts.parent, opts.actor, opts.reason);
|
|
1310
|
+
console.log(`✓ ${policy.id} linked as a non-blocking exception of ${policy.exception_of} (revision ${policy.revision})`);
|
|
1311
|
+
console.log(" proof and authority cleared; composition remains advisory until separately proved.");
|
|
1312
|
+
}
|
|
1313
|
+
catch (e) {
|
|
1314
|
+
fail(e.message);
|
|
1315
|
+
}
|
|
1316
|
+
finally {
|
|
1317
|
+
store.close();
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
policyCmd
|
|
1321
|
+
.command("relations")
|
|
1322
|
+
.description("Inspect explicit exception-parent links without evaluating, composing, activating, or enforcing policy.")
|
|
1323
|
+
.argument("<id>", "policy id")
|
|
1324
|
+
.option("--public-only", "exclude private-overlay policy records")
|
|
1325
|
+
.action((id, opts) => {
|
|
1326
|
+
const { store, root } = storeFor();
|
|
1327
|
+
try {
|
|
1328
|
+
const relations = new ConstitutionService(store, root).relations(id, { publicOnly: opts.publicOnly });
|
|
1329
|
+
console.log(JSON.stringify(relations, null, 2));
|
|
1330
|
+
}
|
|
1331
|
+
catch (e) {
|
|
1332
|
+
fail(e.message);
|
|
1333
|
+
}
|
|
1334
|
+
finally {
|
|
1335
|
+
store.close();
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
policyCmd
|
|
1339
|
+
.command("consolidation")
|
|
1340
|
+
.description("Inspect a non-mutating advisory consolidation candidate from an existing scope suggestion.")
|
|
1341
|
+
.argument("<id>", "anchor policy id")
|
|
1342
|
+
.option("--public-only", "exclude private-overlay policy records")
|
|
1343
|
+
.action((id, opts) => {
|
|
1344
|
+
const { store, root } = storeFor();
|
|
1345
|
+
try {
|
|
1346
|
+
const candidate = new ConstitutionService(store, root).consolidation(id, { publicOnly: opts.publicOnly });
|
|
1347
|
+
console.log(JSON.stringify(candidate, null, 2));
|
|
1348
|
+
}
|
|
1349
|
+
catch (e) {
|
|
1350
|
+
fail(e.message);
|
|
1351
|
+
}
|
|
1352
|
+
finally {
|
|
1353
|
+
store.close();
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
policyCmd
|
|
1357
|
+
.command("evaluate")
|
|
1358
|
+
.description("Evaluate one or all policies with the neutral deterministic result algebra.")
|
|
1359
|
+
.argument("[id]", "optional policy id")
|
|
1360
|
+
.option("--active", "evaluate active policies only")
|
|
1361
|
+
.option("--public-only", "exclude private-overlay policies and graph records")
|
|
1362
|
+
.option("--staged", "evaluate executable-behavior policies against the staged index snapshot")
|
|
1363
|
+
.option("--working", "evaluate executable-behavior policies against all staged, unstaged, and untracked changes")
|
|
1364
|
+
.option("--commit <sha>", "evaluate executable-behavior policies at an exact commit")
|
|
1365
|
+
.option("--strict", "exit non-zero on an authorized blocking violation or evaluator error")
|
|
1366
|
+
.option("--json", "emit canonical receipt objects as JSON")
|
|
1367
|
+
.action((id, opts) => {
|
|
1368
|
+
const { store, root } = storeFor();
|
|
1369
|
+
try {
|
|
1370
|
+
const sources = [opts.staged && "--staged", opts.working && "--working", opts.commit && "--commit"].filter(Boolean);
|
|
1371
|
+
if (sources.length > 1)
|
|
1372
|
+
throw new Error(`pick one executable-behavior snapshot source (got ${sources.join(", ")})`);
|
|
1373
|
+
if (opts.commit && !revExists(opts.commit, root))
|
|
1374
|
+
throw new Error(`--commit ref "${opts.commit}" does not resolve`);
|
|
1375
|
+
const behavior = opts.staged ? { workspace: "staged" }
|
|
1376
|
+
: opts.working ? { workspace: "working" }
|
|
1377
|
+
: opts.commit ? { commit: revParse(opts.commit, root) }
|
|
1378
|
+
: undefined;
|
|
1379
|
+
indexRepo(store, root, { churn: false });
|
|
1380
|
+
store.reindex();
|
|
1381
|
+
const results = new ConstitutionService(store, root).evaluate({ id, activeOnly: opts.active, publicOnly: opts.publicOnly, behavior });
|
|
1382
|
+
if (opts.json)
|
|
1383
|
+
console.log(JSON.stringify(results.map((r) => r.evaluation), null, 2));
|
|
1384
|
+
else
|
|
1385
|
+
renderPolicyEvaluations(results).forEach((line) => console.log(line));
|
|
1386
|
+
if (opts.strict && results.some((r) => r.blocks || r.strict_error))
|
|
1387
|
+
process.exitCode = 1;
|
|
1388
|
+
}
|
|
1389
|
+
catch (e) {
|
|
1390
|
+
fail(e.message);
|
|
1391
|
+
}
|
|
1392
|
+
finally {
|
|
1393
|
+
store.close();
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
function renderPolicyEvaluations(results) {
|
|
1397
|
+
if (!results.length)
|
|
1398
|
+
return ["No Constitution policies matched."];
|
|
1399
|
+
const icon = { satisfied: "✅", violated: "⛔", not_applicable: "·", unknown: "?", error: "‼" };
|
|
1400
|
+
const out = [`Constitution policy evaluation: ${results.length} canonical receipt(s)`];
|
|
1401
|
+
for (const r of results) {
|
|
1402
|
+
out.push(` ${icon[r.evaluation.result] ?? "·"} ${r.policy.id} [${r.policy.state}] ${r.evaluation.result}${r.blocks ? " — BLOCK" : ""}`);
|
|
1403
|
+
out.push(` ${r.evaluation.explanation}`);
|
|
1404
|
+
if (r.gate_error)
|
|
1405
|
+
out.push(` gate error: ${r.gate_error}`);
|
|
1406
|
+
out.push(` receipt: ${r.evaluation.deterministic_hash}`);
|
|
1407
|
+
}
|
|
1408
|
+
return out;
|
|
1409
|
+
}
|
|
1410
|
+
// ---- constitution (deterministic evidence -> candidate bootstrap) --------
|
|
1411
|
+
const constitutionCmd = program
|
|
1412
|
+
.command("constitution")
|
|
1413
|
+
.description("Bootstrap Hunch Constitution candidates from attributable structured evidence.");
|
|
1414
|
+
constitutionCmd
|
|
1415
|
+
.command("scorecard")
|
|
1416
|
+
.description("Validate and score a versioned EXP-03 Intent Compiler case bank; writes nothing.")
|
|
1417
|
+
.argument("[file]", "path to a version-1 EXP-03 JSON case bank", join(dirname(fileURLToPath(import.meta.url)), "../../bench/constitution-exp03-v1.json"))
|
|
1418
|
+
.option("--json", "emit the canonical scorecard as JSON")
|
|
1419
|
+
.action((file, opts) => {
|
|
1420
|
+
try {
|
|
1421
|
+
const scorecard = scoreCompilerCaseBank(JSON.parse(readFileSync(resolve(file), "utf8")));
|
|
1422
|
+
console.log(opts.json ? JSON.stringify(scorecard, null, 2) : renderCompilerScorecard(scorecard));
|
|
1423
|
+
if (!scorecard.passed)
|
|
1424
|
+
process.exitCode = 1;
|
|
1425
|
+
}
|
|
1426
|
+
catch (e) {
|
|
1427
|
+
fail(e.message);
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
constitutionCmd
|
|
1431
|
+
.command("ingest")
|
|
1432
|
+
.description("Normalize local corrections/failures, committed instructions/ADRs, and local review/PR exports into Git-native EvidenceEvents; creates no policy authority.")
|
|
1433
|
+
.option("--since <duration>", "evidence window, e.g. 90d or 12w", "90d")
|
|
1434
|
+
.option("--max-events <n>", "maximum events normalized in one run (hard-capped at 200)", "100")
|
|
1435
|
+
.option("--instructions", "hash-normalize bounded committed instruction and ADR markdown")
|
|
1436
|
+
.option("--from <files...>", "strict local review/conversation/PR export JSON file(s)")
|
|
1437
|
+
.option("--public-only", "read/write only public records")
|
|
1438
|
+
.option("--private", "read/write only the configured private overlay")
|
|
1439
|
+
.action((opts) => {
|
|
1440
|
+
const { store, root } = storeFor();
|
|
1441
|
+
try {
|
|
1442
|
+
const maxEvents = Number(opts.maxEvents);
|
|
1443
|
+
if (!Number.isFinite(maxEvents) || maxEvents <= 0)
|
|
1444
|
+
throw new Error("--max-events must be a positive number");
|
|
1445
|
+
const report = new ConstitutionService(store, root).ingest({
|
|
1446
|
+
since: opts.since,
|
|
1447
|
+
maxEvents,
|
|
1448
|
+
instructions: opts.instructions,
|
|
1449
|
+
importFiles: opts.from,
|
|
1450
|
+
publicOnly: opts.publicOnly,
|
|
1451
|
+
privateOnly: opts.private,
|
|
1452
|
+
});
|
|
1453
|
+
console.log(`Constitution evidence ingest: scanned ${report.scanned} record(s), ${report.eligible} eligible`);
|
|
1454
|
+
console.log(` ${report.normalized} normalized · ${report.existing} existing · ${report.covered} covered · ${report.uncompilable} uncompilable · ${report.excluded} excluded`);
|
|
1455
|
+
for (const event of report.events) {
|
|
1456
|
+
console.log(` ${event.id} [${event.kind}/${event.compiler?.status ?? "normalized"}] ${event.text_ref ?? ""}`);
|
|
1457
|
+
}
|
|
1458
|
+
console.log(" authority: none; ingestion never proves, proposes, activates, or blocks.");
|
|
1459
|
+
}
|
|
1460
|
+
catch (e) {
|
|
1461
|
+
fail(e.message);
|
|
1462
|
+
}
|
|
1463
|
+
finally {
|
|
1464
|
+
store.close();
|
|
1465
|
+
}
|
|
1466
|
+
});
|
|
1467
|
+
constitutionCmd
|
|
1468
|
+
.command("delta")
|
|
1469
|
+
.description("Inspect the exact first-parent structural delta and supported candidates for one human-confirmed fix/revert decision; writes nothing.")
|
|
1470
|
+
.argument("<decision-id>", "git-anchored decision id")
|
|
1471
|
+
.option("--public-only", "read only the public decision and graph")
|
|
1472
|
+
.option("--private", "read only the configured private overlay decision and union graph")
|
|
1473
|
+
.action((decisionId, opts) => {
|
|
1474
|
+
const { store, root } = storeFor();
|
|
1475
|
+
try {
|
|
1476
|
+
const inspection = new ConstitutionService(store, root).inspectStructural(decisionId, {
|
|
1477
|
+
publicOnly: opts.publicOnly,
|
|
1478
|
+
privateOnly: opts.private,
|
|
1479
|
+
});
|
|
1480
|
+
console.log(JSON.stringify(inspection, null, 2));
|
|
1481
|
+
}
|
|
1482
|
+
catch (e) {
|
|
1483
|
+
fail(e.message);
|
|
1484
|
+
}
|
|
1485
|
+
finally {
|
|
1486
|
+
store.close();
|
|
1487
|
+
}
|
|
1488
|
+
});
|
|
1489
|
+
constitutionCmd
|
|
1490
|
+
.command("bootstrap")
|
|
1491
|
+
.description("Normalize eligible decisions into evidence events and compile at most three non-active Policy IR candidates.")
|
|
1492
|
+
.option("--since <duration>", "evidence window, e.g. 90d or 12w", "90d")
|
|
1493
|
+
.option("--max-candidates <n>", "maximum surfaced candidates (hard-capped at 3)", "3")
|
|
1494
|
+
.option("--public-only", "read/write only public evidence and policies")
|
|
1495
|
+
.option("--private", "read/write only the configured private overlay")
|
|
1496
|
+
.option("--history", "use exact first-parent deltas from human-confirmed fix/revert decisions; ambiguous deltas remain uncompilable")
|
|
1497
|
+
.action((opts) => {
|
|
1498
|
+
const { store, root } = storeFor();
|
|
1499
|
+
try {
|
|
1500
|
+
const requested = Number(opts.maxCandidates);
|
|
1501
|
+
if (!Number.isFinite(requested) || requested <= 0)
|
|
1502
|
+
throw new Error("--max-candidates must be a positive number");
|
|
1503
|
+
if (opts.history) {
|
|
1504
|
+
indexRepo(store, root, { churn: false });
|
|
1505
|
+
store.reindex();
|
|
1506
|
+
}
|
|
1507
|
+
const report = new ConstitutionService(store, root).bootstrap({
|
|
1508
|
+
since: opts.since,
|
|
1509
|
+
maxCandidates: requested,
|
|
1510
|
+
publicOnly: opts.publicOnly,
|
|
1511
|
+
privateOnly: opts.private,
|
|
1512
|
+
history: opts.history,
|
|
1513
|
+
});
|
|
1514
|
+
console.log(`Constitution ${opts.history ? "history " : ""}bootstrap: scanned ${report.scanned} decision(s), ${report.eligible} eligible`);
|
|
1515
|
+
for (const candidate of report.compiled) {
|
|
1516
|
+
console.log(` + ${candidate.policy.id} [${candidate.policy.state}] ${candidate.policy.statement}`);
|
|
1517
|
+
console.log(` evidence: ${candidate.evidence.id} · ${candidate.policy.assertion.kind} · authority: none`);
|
|
1518
|
+
}
|
|
1519
|
+
console.log(` ${report.compiled.length} compiled · ${report.covered} already covered · ${report.conflicted} conflicted · ${report.deferred} deferred by max-three cap · ${report.uncompilable} uncompilable`);
|
|
1520
|
+
if (!report.compiled.length)
|
|
1521
|
+
console.log(" No new candidates; existing policy lifecycle states were left untouched.");
|
|
1522
|
+
}
|
|
1523
|
+
catch (e) {
|
|
1524
|
+
fail(e.message);
|
|
1525
|
+
}
|
|
1526
|
+
finally {
|
|
1527
|
+
store.close();
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
constitutionCmd
|
|
1531
|
+
.command("g2")
|
|
1532
|
+
.description("Inspect exact private G2 evidence; optionally append a human plan, runbook rehearsal, or candidate review. Never grants policy authority.")
|
|
1533
|
+
.option("--plan <file>", "append a private human-selected G2 plan from JSON")
|
|
1534
|
+
.option("--rehearse <runbook-id>", "append a rehearsal receipt for the exact current private runbook content")
|
|
1535
|
+
.option("--attest <candidate-id>", "append an exact private human selection or rejection for one reviewed candidate")
|
|
1536
|
+
.option("--observe", "record at most one private shadow observation per selected policy for the current real HEAD/graph")
|
|
1537
|
+
.option("--backfill <n>", "atomically record shadow observations across up to n real first-parent commits after an all-policy preflight")
|
|
1538
|
+
.option("--drill <category>", "execute one exact selected operational runbook drill, or all seven with category=all; writes no evidence")
|
|
1539
|
+
.option("--queue <limit>", "return the bounded current-proof queue of unclassified G2 shadow violations")
|
|
1540
|
+
.option("--candidates <limit>", "return a bounded read-only review packet of exact fix-history candidates requiring human attestation")
|
|
1541
|
+
.option("--behavior-candidates <limit>", "derive bounded behavior-level candidates from rejected grounded proxies plus newly added regression tests")
|
|
1542
|
+
.option("--behavior-decision <decision-id>", "scope behavior review/replay/materialization to one exact current human-confirmed decision")
|
|
1543
|
+
.option("--behavior-replay <candidate-id>", "run one exact behavior candidate in disposable known-bad/good worktrees")
|
|
1544
|
+
.option("--behavior-deps <candidate-id>", "explicitly build or validate content-addressed historical dependency snapshots for one behavior candidate")
|
|
1545
|
+
.option("--behavior-attest <candidate-id>", "append an exact private human selection or rejection bound to a snapshot-backed behavior replay")
|
|
1546
|
+
.option("--behavior-materialize", "assess current selected behavior attestations against the exact supported Policy IR without creating artifacts")
|
|
1547
|
+
.option("--behavior-policy-materialize", "materialize and P3-prove every current selected behavior as a private non-authoritative policy proposal")
|
|
1548
|
+
.option("--behavior-review-hash <hash>", "exact behavior-candidate review content hash being replayed or provisioned")
|
|
1549
|
+
.option("--allow-install-script <packages...>", "dependency packages explicitly allowed to run lifecycle scripts while building --behavior-deps")
|
|
1550
|
+
.option("--dependency-timeout-ms <n>", "timeout for each npm dependency snapshot operation", "300000")
|
|
1551
|
+
.option("--candidate-since <window>", "git history window for candidate review or attestation", "180d")
|
|
1552
|
+
.option("--candidate-commits <n>", "maximum fix-labeled commits inspected for candidate review or attestation", "100")
|
|
1553
|
+
.option("--candidate-limit <n>", "exact review-packet limit used by --attest or --behavior-replay", "30")
|
|
1554
|
+
.option("--review-hash <hash>", "exact candidate review content hash being attested")
|
|
1555
|
+
.option("--disposition <value>", "candidate disposition: selected | rejected")
|
|
1556
|
+
.option("--result <result>", "rehearsal result: passed | failed")
|
|
1557
|
+
.option("--actor <actor>", "explicit human actor (human:, github:, or git:)")
|
|
1558
|
+
.option("--evidence <hashes...>", "one or more sha1 evidence hashes for a rehearsal")
|
|
1559
|
+
.option("--notes <text>", "rehearsal evidence notes")
|
|
1560
|
+
.option("--reason <text>", "candidate selection or rejection rationale")
|
|
1561
|
+
.option("--supersedes <id>", "current rehearsal or candidate attestation corrected by this append-only receipt")
|
|
1562
|
+
.option("--strict", "exit nonzero while the packet is not eligible for explicit human G2 signoff")
|
|
1563
|
+
.action((opts) => {
|
|
1564
|
+
const { store, root } = storeFor();
|
|
1565
|
+
try {
|
|
1566
|
+
const queueRequested = opts.queue !== undefined;
|
|
1567
|
+
const candidatesRequested = opts.candidates !== undefined;
|
|
1568
|
+
const behaviorCandidatesRequested = opts.behaviorCandidates !== undefined;
|
|
1569
|
+
const behaviorReplayRequested = opts.behaviorReplay !== undefined;
|
|
1570
|
+
const behaviorDepsRequested = opts.behaviorDeps !== undefined;
|
|
1571
|
+
const behaviorAttestRequested = opts.behaviorAttest !== undefined;
|
|
1572
|
+
const behaviorMaterializeRequested = opts.behaviorMaterialize === true;
|
|
1573
|
+
const behaviorPolicyMaterializeRequested = opts.behaviorPolicyMaterialize === true;
|
|
1574
|
+
const backfillRequested = opts.backfill !== undefined;
|
|
1575
|
+
const drillRequested = opts.drill !== undefined;
|
|
1576
|
+
const behaviorActionRequested = behaviorCandidatesRequested || behaviorReplayRequested || behaviorDepsRequested
|
|
1577
|
+
|| behaviorAttestRequested || behaviorMaterializeRequested || behaviorPolicyMaterializeRequested;
|
|
1578
|
+
const attestRequested = opts.attest !== undefined;
|
|
1579
|
+
const actions = [!!opts.plan, !!opts.rehearse, attestRequested, !!opts.observe, backfillRequested, drillRequested, queueRequested, candidatesRequested, behaviorCandidatesRequested, behaviorReplayRequested, behaviorDepsRequested, behaviorAttestRequested, behaviorMaterializeRequested, behaviorPolicyMaterializeRequested].filter(Boolean).length;
|
|
1580
|
+
if (actions > 1)
|
|
1581
|
+
throw new Error("choose only one G2 plan, rehearsal, structural attestation, observation, historical backfill, operational drill, queue, structural candidate, behavior candidate, behavior replay, dependency snapshot, behavior attestation, behavior assessment, or behavior policy materialization action");
|
|
1582
|
+
if (opts.allowInstallScript && !behaviorDepsRequested && !behaviorPolicyMaterializeRequested)
|
|
1583
|
+
throw new Error("--allow-install-script requires --behavior-deps or --behavior-policy-materialize");
|
|
1584
|
+
if (opts.behaviorDecision && !behaviorActionRequested)
|
|
1585
|
+
throw new Error("--behavior-decision requires a behavior candidate, replay, dependency, attestation, assessment, or materialization action");
|
|
1586
|
+
const service = new ConstitutionService(store, root);
|
|
1587
|
+
let output;
|
|
1588
|
+
if (opts.plan) {
|
|
1589
|
+
const appended = service.createG2Plan(JSON.parse(readFileSync(resolve(opts.plan), "utf8")));
|
|
1590
|
+
output = { appended, readiness: service.g2Readiness() };
|
|
1591
|
+
}
|
|
1592
|
+
else if (attestRequested) {
|
|
1593
|
+
if (opts.result || opts.evidence || opts.notes)
|
|
1594
|
+
throw new Error("--attest does not accept rehearsal --result, --evidence, or --notes");
|
|
1595
|
+
if (!opts.reviewHash)
|
|
1596
|
+
throw new Error("--attest requires --review-hash");
|
|
1597
|
+
if (opts.disposition !== "selected" && opts.disposition !== "rejected")
|
|
1598
|
+
throw new Error("--attest requires --disposition selected|rejected");
|
|
1599
|
+
if (!opts.actor)
|
|
1600
|
+
throw new Error("--attest requires --actor");
|
|
1601
|
+
if (!opts.reason)
|
|
1602
|
+
throw new Error("--attest requires --reason");
|
|
1603
|
+
const reviewOptions = {
|
|
1604
|
+
since: opts.candidateSince,
|
|
1605
|
+
maxCommits: Number(opts.candidateCommits),
|
|
1606
|
+
limit: Number(opts.candidateLimit),
|
|
1607
|
+
};
|
|
1608
|
+
const appended = service.attestG2Candidate(opts.attest, opts.reviewHash, opts.disposition, opts.actor, opts.reason, {
|
|
1609
|
+
...reviewOptions,
|
|
1610
|
+
supersedes: opts.supersedes,
|
|
1611
|
+
});
|
|
1612
|
+
output = { appended, review: service.g2CandidateReview(reviewOptions) };
|
|
1613
|
+
}
|
|
1614
|
+
else if (opts.rehearse) {
|
|
1615
|
+
if (opts.reviewHash || opts.disposition || opts.reason)
|
|
1616
|
+
throw new Error("--rehearse does not accept candidate --review-hash, --disposition, or --reason");
|
|
1617
|
+
if (opts.result !== "passed" && opts.result !== "failed")
|
|
1618
|
+
throw new Error("--rehearse requires --result passed|failed");
|
|
1619
|
+
if (!opts.actor)
|
|
1620
|
+
throw new Error("--rehearse requires --actor");
|
|
1621
|
+
if (!opts.evidence?.length)
|
|
1622
|
+
throw new Error("--rehearse requires at least one --evidence sha1 hash");
|
|
1623
|
+
if (!opts.notes)
|
|
1624
|
+
throw new Error("--rehearse requires --notes");
|
|
1625
|
+
const appended = service.recordRunbookRehearsal(opts.rehearse, opts.result, opts.actor, opts.evidence, opts.notes, { supersedes: opts.supersedes });
|
|
1626
|
+
output = { appended, readiness: service.g2Readiness() };
|
|
1627
|
+
}
|
|
1628
|
+
else if (opts.observe) {
|
|
1629
|
+
indexRepo(store, root, { churn: false });
|
|
1630
|
+
store.reindex();
|
|
1631
|
+
output = { sweep: service.g2ShadowSweep(), readiness: service.g2Readiness() };
|
|
1632
|
+
}
|
|
1633
|
+
else if (backfillRequested) {
|
|
1634
|
+
output = { backfill: service.g2ShadowBackfill(Number(opts.backfill)), readiness: service.g2Readiness() };
|
|
1635
|
+
}
|
|
1636
|
+
else if (drillRequested) {
|
|
1637
|
+
const categories = opts.drill === "all" ? [...G2_RUNBOOK_CATEGORIES] : G2_RUNBOOK_CATEGORIES.filter((category) => category === opts.drill);
|
|
1638
|
+
if (!categories.length)
|
|
1639
|
+
throw new Error(`--drill must be all or one of: ${G2_RUNBOOK_CATEGORIES.join(", ")}`);
|
|
1640
|
+
output = { drills: categories.map((category) => service.g2OperationalDrill(category)), readiness: service.g2Readiness() };
|
|
1641
|
+
}
|
|
1642
|
+
else if (queueRequested) {
|
|
1643
|
+
output = service.g2ShadowQueue(Number(opts.queue));
|
|
1644
|
+
}
|
|
1645
|
+
else if (candidatesRequested) {
|
|
1646
|
+
output = service.g2CandidateReview({ since: opts.candidateSince, maxCommits: Number(opts.candidateCommits), limit: Number(opts.candidates) });
|
|
1647
|
+
}
|
|
1648
|
+
else if (behaviorCandidatesRequested) {
|
|
1649
|
+
if (opts.behaviorReviewHash)
|
|
1650
|
+
throw new Error("--behavior-candidates does not accept --behavior-review-hash");
|
|
1651
|
+
output = service.g2BehaviorCandidateReview({ since: opts.candidateSince, maxCommits: Number(opts.candidateCommits), limit: Number(opts.behaviorCandidates), decisionId: opts.behaviorDecision });
|
|
1652
|
+
}
|
|
1653
|
+
else if (behaviorReplayRequested) {
|
|
1654
|
+
if (!opts.behaviorReviewHash)
|
|
1655
|
+
throw new Error("--behavior-replay requires --behavior-review-hash");
|
|
1656
|
+
if (opts.result || opts.actor || opts.evidence || opts.notes || opts.reason || opts.reviewHash || opts.disposition || opts.supersedes || opts.allowInstallScript) {
|
|
1657
|
+
throw new Error("--behavior-replay accepts no human evidence or structural-attestation options");
|
|
1658
|
+
}
|
|
1659
|
+
output = service.g2BehaviorCandidateReplay(opts.behaviorReplay, opts.behaviorReviewHash, {
|
|
1660
|
+
since: opts.candidateSince,
|
|
1661
|
+
maxCommits: Number(opts.candidateCommits),
|
|
1662
|
+
limit: Number(opts.candidateLimit),
|
|
1663
|
+
decisionId: opts.behaviorDecision,
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
else if (behaviorDepsRequested) {
|
|
1667
|
+
if (!opts.behaviorReviewHash)
|
|
1668
|
+
throw new Error("--behavior-deps requires --behavior-review-hash");
|
|
1669
|
+
if (opts.result || opts.actor || opts.evidence || opts.notes || opts.reason || opts.reviewHash || opts.disposition || opts.supersedes) {
|
|
1670
|
+
throw new Error("--behavior-deps accepts no human evidence or structural-attestation options");
|
|
1671
|
+
}
|
|
1672
|
+
output = service.g2BehaviorDependencySnapshots(opts.behaviorDeps, opts.behaviorReviewHash, {
|
|
1673
|
+
since: opts.candidateSince,
|
|
1674
|
+
maxCommits: Number(opts.candidateCommits),
|
|
1675
|
+
limit: Number(opts.candidateLimit),
|
|
1676
|
+
decisionId: opts.behaviorDecision,
|
|
1677
|
+
allowInstallScripts: opts.allowInstallScript ?? [],
|
|
1678
|
+
timeoutMs: Number(opts.dependencyTimeoutMs),
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
else if (behaviorAttestRequested) {
|
|
1682
|
+
if (!opts.behaviorReviewHash)
|
|
1683
|
+
throw new Error("--behavior-attest requires --behavior-review-hash");
|
|
1684
|
+
if (opts.disposition !== "selected" && opts.disposition !== "rejected")
|
|
1685
|
+
throw new Error("--behavior-attest requires --disposition selected|rejected");
|
|
1686
|
+
if (!opts.actor)
|
|
1687
|
+
throw new Error("--behavior-attest requires --actor");
|
|
1688
|
+
if (!opts.reason)
|
|
1689
|
+
throw new Error("--behavior-attest requires --reason");
|
|
1690
|
+
if (opts.result || opts.evidence || opts.notes || opts.reviewHash || opts.allowInstallScript) {
|
|
1691
|
+
throw new Error("--behavior-attest accepts no rehearsal, structural-review, or dependency-install options");
|
|
1692
|
+
}
|
|
1693
|
+
const reviewOptions = {
|
|
1694
|
+
since: opts.candidateSince,
|
|
1695
|
+
maxCommits: Number(opts.candidateCommits),
|
|
1696
|
+
limit: Number(opts.candidateLimit),
|
|
1697
|
+
decisionId: opts.behaviorDecision,
|
|
1698
|
+
};
|
|
1699
|
+
const appended = service.attestG2BehaviorCandidate(opts.behaviorAttest, opts.behaviorReviewHash, opts.disposition, opts.actor, opts.reason, { ...reviewOptions, supersedes: opts.supersedes });
|
|
1700
|
+
output = { appended, review: service.g2BehaviorCandidateReview(reviewOptions) };
|
|
1701
|
+
}
|
|
1702
|
+
else if (behaviorMaterializeRequested) {
|
|
1703
|
+
if (opts.result || opts.actor || opts.evidence || opts.notes || opts.reason || opts.reviewHash || opts.disposition || opts.supersedes || opts.behaviorReviewHash || opts.allowInstallScript) {
|
|
1704
|
+
throw new Error("--behavior-materialize accepts no human evidence, review-hash, or dependency-install options");
|
|
1705
|
+
}
|
|
1706
|
+
output = service.g2BehaviorMaterializationAssessment({
|
|
1707
|
+
since: opts.candidateSince,
|
|
1708
|
+
maxCommits: Number(opts.candidateCommits),
|
|
1709
|
+
limit: Number(opts.candidateLimit),
|
|
1710
|
+
decisionId: opts.behaviorDecision,
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
else if (behaviorPolicyMaterializeRequested) {
|
|
1714
|
+
if (opts.result || opts.actor || opts.evidence || opts.notes || opts.reason || opts.reviewHash || opts.disposition || opts.supersedes || opts.behaviorReviewHash) {
|
|
1715
|
+
throw new Error("--behavior-policy-materialize accepts no human evidence or review-hash options");
|
|
1716
|
+
}
|
|
1717
|
+
output = service.g2BehaviorPolicyMaterialize({
|
|
1718
|
+
since: opts.candidateSince,
|
|
1719
|
+
maxCommits: Number(opts.candidateCommits),
|
|
1720
|
+
limit: Number(opts.candidateLimit),
|
|
1721
|
+
decisionId: opts.behaviorDecision,
|
|
1722
|
+
allowInstallScripts: opts.allowInstallScript ?? [],
|
|
1723
|
+
dependencyTimeoutMs: Number(opts.dependencyTimeoutMs),
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
else if (opts.result || opts.actor || opts.evidence || opts.notes || opts.reason || opts.reviewHash || opts.disposition || opts.supersedes || opts.behaviorReviewHash || opts.allowInstallScript) {
|
|
1727
|
+
throw new Error("human evidence options require --rehearse <runbook-id> or --attest <candidate-id>");
|
|
1728
|
+
}
|
|
1729
|
+
else {
|
|
1730
|
+
output = service.g2Readiness();
|
|
1731
|
+
}
|
|
1732
|
+
const readiness = service.g2Readiness();
|
|
1733
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1734
|
+
if (opts.strict && readiness.recommendation !== "eligible_for_human_g2_signoff")
|
|
1735
|
+
process.exitCode = 1;
|
|
1736
|
+
}
|
|
1737
|
+
catch (e) {
|
|
1738
|
+
fail(e.message);
|
|
1739
|
+
}
|
|
1740
|
+
finally {
|
|
1741
|
+
store.close();
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
constitutionCmd
|
|
1745
|
+
.command("g3")
|
|
1746
|
+
.description("Inspect exact private G3 advisory evidence; optionally append a preregistration, human plan, proof review, or executable adapter receipt. Never signs off G3.")
|
|
1747
|
+
.option("--experiment <file>", "append an immutable private EXP-01 or EXP-03 preregistration from JSON")
|
|
1748
|
+
.option("--plan <file>", "append a private human-selected G3 plan bound to exact G2 readiness and experiment records")
|
|
1749
|
+
.option("--review <file>", "append a measured human proof-card review for one plan-selected policy")
|
|
1750
|
+
.option("--conformance", "execute and append the exact current plan's supported three-client adapter fixture")
|
|
1751
|
+
.option("--timeout-ms <n>", "timeout for the executable adapter conformance fixture", "180000")
|
|
1752
|
+
.option("--strict", "exit nonzero while the packet is not eligible for explicit human G3 signoff")
|
|
1753
|
+
.action((opts) => {
|
|
1754
|
+
const { store, root } = storeFor();
|
|
1755
|
+
try {
|
|
1756
|
+
const actions = [!!opts.experiment, !!opts.plan, !!opts.review, !!opts.conformance].filter(Boolean).length;
|
|
1757
|
+
if (actions > 1)
|
|
1758
|
+
throw new Error("choose only one G3 preregistration, plan, proof review, or adapter-conformance action");
|
|
1759
|
+
const service = new ConstitutionService(store, root);
|
|
1760
|
+
let output;
|
|
1761
|
+
if (opts.experiment) {
|
|
1762
|
+
const appended = service.registerG3Experiment(JSON.parse(readFileSync(resolve(opts.experiment), "utf8")));
|
|
1763
|
+
output = { appended, readiness: service.g3Readiness() };
|
|
1764
|
+
}
|
|
1765
|
+
else if (opts.plan) {
|
|
1766
|
+
const appended = service.createG3Plan(JSON.parse(readFileSync(resolve(opts.plan), "utf8")));
|
|
1767
|
+
output = { appended, readiness: service.g3Readiness() };
|
|
1768
|
+
}
|
|
1769
|
+
else if (opts.review) {
|
|
1770
|
+
const input = JSON.parse(readFileSync(resolve(opts.review), "utf8"));
|
|
1771
|
+
const appended = service.recordG3ProofReview(input);
|
|
1772
|
+
output = { appended, readiness: service.g3Readiness() };
|
|
1773
|
+
}
|
|
1774
|
+
else if (opts.conformance) {
|
|
1775
|
+
const appended = service.g3AdapterConformance({ timeoutMs: Number(opts.timeoutMs) });
|
|
1776
|
+
output = { appended, readiness: service.g3Readiness() };
|
|
1777
|
+
}
|
|
1778
|
+
else {
|
|
1779
|
+
output = service.g3Readiness();
|
|
1780
|
+
}
|
|
1781
|
+
const readiness = service.g3Readiness();
|
|
1782
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1783
|
+
if (opts.strict && readiness.recommendation !== "eligible_for_human_g3_signoff")
|
|
1784
|
+
process.exitCode = 1;
|
|
1785
|
+
}
|
|
1786
|
+
catch (e) {
|
|
1787
|
+
fail(e.message);
|
|
1788
|
+
}
|
|
1789
|
+
finally {
|
|
1790
|
+
store.close();
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
// ---- experiment (fresh preregistration-bound execution) -----------------
|
|
1794
|
+
const experimentCmd = program
|
|
1795
|
+
.command("experiment")
|
|
1796
|
+
.description("Lock, execute, review, and report fresh private preregistered experiments. No result grants policy authority.");
|
|
1797
|
+
experimentCmd
|
|
1798
|
+
.command("validate")
|
|
1799
|
+
.description("Validate and content-address a draft case bank against the current preregistration without writing it.")
|
|
1800
|
+
.argument("<file>", "case bank JSON input")
|
|
1801
|
+
.action((file) => {
|
|
1802
|
+
const { store, root } = storeFor();
|
|
1803
|
+
try {
|
|
1804
|
+
const input = JSON.parse(readFileSync(resolve(file), "utf8"));
|
|
1805
|
+
console.log(JSON.stringify(new ConstitutionService(store, root).validateExperimentCaseBank(input), null, 2));
|
|
1806
|
+
}
|
|
1807
|
+
catch (e) {
|
|
1808
|
+
fail(e.message);
|
|
1809
|
+
}
|
|
1810
|
+
finally {
|
|
1811
|
+
store.close();
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
experimentCmd
|
|
1815
|
+
.command("prepare")
|
|
1816
|
+
.description("Lock a fresh private case bank to the exact current preregistration before assignment.")
|
|
1817
|
+
.argument("<file>", "case bank JSON input")
|
|
1818
|
+
.action((file) => {
|
|
1819
|
+
const { store, root } = storeFor();
|
|
1820
|
+
try {
|
|
1821
|
+
const input = JSON.parse(readFileSync(resolve(file), "utf8"));
|
|
1822
|
+
console.log(JSON.stringify(new ConstitutionService(store, root).lockExperimentCaseBank(input), null, 2));
|
|
1823
|
+
}
|
|
1824
|
+
catch (e) {
|
|
1825
|
+
fail(e.message);
|
|
1826
|
+
}
|
|
1827
|
+
finally {
|
|
1828
|
+
store.close();
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
experimentCmd
|
|
1832
|
+
.command("create")
|
|
1833
|
+
.description("Create one immutable balanced assignment manifest; one run is allowed per preregistration.")
|
|
1834
|
+
.argument("<case-bank-id>", "locked private case bank id")
|
|
1835
|
+
.requiredOption("--sample-per-arm <n>", "full preregistered target per arm; the minimum is only a checkpoint")
|
|
1836
|
+
.requiredOption("--actor <actor>", "explicit owner (human:, git:, or github:)")
|
|
1837
|
+
.requiredOption("--reason <text>", "why this exact run manifest is being created")
|
|
1838
|
+
.option("--provider <provider>", "EXP-01 subscription CLI: claude-cli | codex-cli")
|
|
1839
|
+
.option("--model <model>", "exact EXP-01 model version or alias")
|
|
1840
|
+
.option("--max-turns <n>", "maximum agent turns for every EXP-01 assignment", "40")
|
|
1841
|
+
.action((caseBankId, opts) => {
|
|
1842
|
+
const { store, root } = storeFor();
|
|
1843
|
+
try {
|
|
1844
|
+
const service = new ConstitutionService(store, root);
|
|
1845
|
+
const bank = service.experimentRepository.listCaseBanks().find((item) => item.id === caseBankId);
|
|
1846
|
+
if (!bank)
|
|
1847
|
+
throw new Error(`unknown private experiment case bank: ${caseBankId}`);
|
|
1848
|
+
if (bank.experiment === "EXP-01" && opts.provider !== "claude-cli" && opts.provider !== "codex-cli") {
|
|
1849
|
+
throw new Error("EXP-01 run creation requires explicit --provider claude-cli|codex-cli; Hunch never chooses which subscription to spend");
|
|
1850
|
+
}
|
|
1851
|
+
if (bank.experiment === "EXP-01" && !opts.model)
|
|
1852
|
+
throw new Error("EXP-01 run creation requires an exact --model stratum");
|
|
1853
|
+
if (bank.experiment === "EXP-03" && (opts.provider || opts.model))
|
|
1854
|
+
throw new Error("EXP-03 is a human-review experiment and accepts no model provider");
|
|
1855
|
+
const provider = opts.provider;
|
|
1856
|
+
const appended = service.createExperimentRun(caseBankId, {
|
|
1857
|
+
sample_per_arm: Number(opts.samplePerArm),
|
|
1858
|
+
...(provider ? { provider, provider_version: subscriptionCliVersion(provider), model_version: opts.model, max_turns: Number(opts.maxTurns) } : {}),
|
|
1859
|
+
actor: opts.actor,
|
|
1860
|
+
reason: opts.reason,
|
|
1861
|
+
});
|
|
1862
|
+
console.log(JSON.stringify({ appended, report: service.experimentReport(appended.id) }, null, 2));
|
|
1863
|
+
}
|
|
1864
|
+
catch (e) {
|
|
1865
|
+
fail(e.message);
|
|
1866
|
+
}
|
|
1867
|
+
finally {
|
|
1868
|
+
store.close();
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
experimentCmd
|
|
1872
|
+
.command("run")
|
|
1873
|
+
.description("Execute the next pending EXP-01 assignments with the manifest-selected subscription CLI.")
|
|
1874
|
+
.argument("<run-id>", "immutable experiment run id")
|
|
1875
|
+
.option("--limit <n>", "maximum assignments to execute in this invocation", "1")
|
|
1876
|
+
.option("--timeout-ms <n>", "per-assignment subscription CLI timeout", "1800000")
|
|
1877
|
+
.action((runId, opts) => {
|
|
1878
|
+
const { store, root } = storeFor();
|
|
1879
|
+
try {
|
|
1880
|
+
console.log(JSON.stringify(new ConstitutionService(store, root).executeExperimentRun(runId, {
|
|
1881
|
+
limit: Number(opts.limit),
|
|
1882
|
+
timeoutMs: Number(opts.timeoutMs),
|
|
1883
|
+
}), null, 2));
|
|
1884
|
+
}
|
|
1885
|
+
catch (e) {
|
|
1886
|
+
fail(e.message);
|
|
1887
|
+
}
|
|
1888
|
+
finally {
|
|
1889
|
+
store.close();
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
experimentCmd
|
|
1893
|
+
.command("next")
|
|
1894
|
+
.description("Start or resume the next randomized EXP-03 human review and return only its assigned treatment.")
|
|
1895
|
+
.argument("<run-id>", "immutable experiment run id")
|
|
1896
|
+
.requiredOption("--reviewer <actor>", "explicit human reviewer")
|
|
1897
|
+
.action((runId, opts) => {
|
|
1898
|
+
const { store, root } = storeFor();
|
|
1899
|
+
try {
|
|
1900
|
+
console.log(JSON.stringify(new ConstitutionService(store, root).nextExperimentReview(runId, opts.reviewer), null, 2));
|
|
1901
|
+
}
|
|
1902
|
+
catch (e) {
|
|
1903
|
+
fail(e.message);
|
|
1904
|
+
}
|
|
1905
|
+
finally {
|
|
1906
|
+
store.close();
|
|
1907
|
+
}
|
|
1908
|
+
});
|
|
1909
|
+
experimentCmd
|
|
1910
|
+
.command("submit")
|
|
1911
|
+
.description("Complete a machine-timed EXP-03 review; duration is derived from the append-only start record.")
|
|
1912
|
+
.argument("<run-id>", "immutable experiment run id")
|
|
1913
|
+
.argument("<assignment-id>", "assignment returned by experiment next")
|
|
1914
|
+
.argument("<file>", "review outcome JSON without a duration field")
|
|
1915
|
+
.action((runId, assignmentId, file) => {
|
|
1916
|
+
const { store, root } = storeFor();
|
|
1917
|
+
try {
|
|
1918
|
+
const input = JSON.parse(readFileSync(resolve(file), "utf8"));
|
|
1919
|
+
const service = new ConstitutionService(store, root);
|
|
1920
|
+
const appended = service.submitExperimentReview(runId, assignmentId, input);
|
|
1921
|
+
console.log(JSON.stringify({ appended, report: service.experimentReport(runId) }, null, 2));
|
|
1922
|
+
}
|
|
1923
|
+
catch (e) {
|
|
1924
|
+
fail(e.message);
|
|
1925
|
+
}
|
|
1926
|
+
finally {
|
|
1927
|
+
store.close();
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
experimentCmd
|
|
1931
|
+
.command("followup")
|
|
1932
|
+
.description("Append the preregistered seven-day EXP-03 reversal measurement.")
|
|
1933
|
+
.argument("<run-id>", "immutable experiment run id")
|
|
1934
|
+
.argument("<assignment-id>", "completed review assignment")
|
|
1935
|
+
.argument("<file>", "follow-up JSON")
|
|
1936
|
+
.action((runId, assignmentId, file) => {
|
|
1937
|
+
const { store, root } = storeFor();
|
|
1938
|
+
try {
|
|
1939
|
+
const input = JSON.parse(readFileSync(resolve(file), "utf8"));
|
|
1940
|
+
const service = new ConstitutionService(store, root);
|
|
1941
|
+
const appended = service.recordExperimentFollowup(runId, assignmentId, input);
|
|
1942
|
+
console.log(JSON.stringify({ appended, report: service.experimentReport(runId) }, null, 2));
|
|
1943
|
+
}
|
|
1944
|
+
catch (e) {
|
|
1945
|
+
fail(e.message);
|
|
1946
|
+
}
|
|
1947
|
+
finally {
|
|
1948
|
+
store.close();
|
|
1949
|
+
}
|
|
1950
|
+
});
|
|
1951
|
+
experimentCmd
|
|
1952
|
+
.command("stop")
|
|
1953
|
+
.description("Irreversibly stop a run for a preregistered safety/privacy or provider-wide inability condition.")
|
|
1954
|
+
.argument("<run-id>", "immutable experiment run id")
|
|
1955
|
+
.argument("<file>", "stop receipt JSON with category, actor, reason, and evidence_hashes")
|
|
1956
|
+
.action((runId, file) => {
|
|
1957
|
+
const { store, root } = storeFor();
|
|
1958
|
+
try {
|
|
1959
|
+
const input = JSON.parse(readFileSync(resolve(file), "utf8"));
|
|
1960
|
+
const service = new ConstitutionService(store, root);
|
|
1961
|
+
const appended = service.stopExperiment(runId, input);
|
|
1962
|
+
console.log(JSON.stringify({ appended, report: service.experimentReport(runId) }, null, 2));
|
|
1963
|
+
}
|
|
1964
|
+
catch (e) {
|
|
1965
|
+
fail(e.message);
|
|
1966
|
+
}
|
|
1967
|
+
finally {
|
|
1968
|
+
store.close();
|
|
1969
|
+
}
|
|
1970
|
+
});
|
|
1971
|
+
for (const command of ["status", "report"]) {
|
|
1972
|
+
experimentCmd
|
|
1973
|
+
.command(command)
|
|
1974
|
+
.description(command === "status" ? "Show raw denominators and remaining assignments." : "Emit the deterministic preregistration-bound analysis receipt.")
|
|
1975
|
+
.argument("<run-id>", "immutable experiment run id")
|
|
1976
|
+
.action((runId) => {
|
|
1977
|
+
const { store, root } = storeFor();
|
|
1978
|
+
try {
|
|
1979
|
+
console.log(JSON.stringify(new ConstitutionService(store, root).experimentReport(runId), null, 2));
|
|
1980
|
+
}
|
|
1981
|
+
catch (e) {
|
|
1982
|
+
fail(e.message);
|
|
1983
|
+
}
|
|
1984
|
+
finally {
|
|
1985
|
+
store.close();
|
|
1986
|
+
}
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
925
1989
|
// ---- compare (rank N candidate solutions by architectural fit) ------------
|
|
926
1990
|
program
|
|
927
1991
|
.command("compare")
|
|
@@ -1287,6 +2351,10 @@ program
|
|
|
1287
2351
|
store.close();
|
|
1288
2352
|
return fail(`--base ref "${opts.base}" does not resolve. In CI, fetch the base branch first (git fetch origin <branch>).`);
|
|
1289
2353
|
}
|
|
2354
|
+
if (opts.commit && !revExists(opts.commit, root)) {
|
|
2355
|
+
store.close();
|
|
2356
|
+
return fail(`--commit ref "${opts.commit}" does not resolve.`);
|
|
2357
|
+
}
|
|
1290
2358
|
store.reindex(); // blast radius walks the edge graph — make the index current
|
|
1291
2359
|
const files = opts.commit ? commitFiles(opts.commit, root)
|
|
1292
2360
|
: opts.base ? rangeFiles(opts.base, root)
|
|
@@ -1325,7 +2393,9 @@ program
|
|
|
1325
2393
|
// gate cases (--staged / --base / --commit HEAD) all have the working tree AT the change.
|
|
1326
2394
|
// Surfaced always; gates the commit/PR under --strict, with the receipt of the why.
|
|
1327
2395
|
const hasConformance = store.recs("decisions").some((d) => (d.conformance?.length ?? 0) > 0);
|
|
1328
|
-
|
|
2396
|
+
const constitution = new ConstitutionService(store, root);
|
|
2397
|
+
const hasActivePolicies = constitution.list({ publicOnly: !!opts.publicOnly }).some((p) => p.state === "active_advisory" || p.state === "active_blocking");
|
|
2398
|
+
if (hasConformance || hasActivePolicies) {
|
|
1329
2399
|
indexRepo(store, root, { churn: false }); // refresh the symbol/dep graph from the working tree
|
|
1330
2400
|
store.reindex();
|
|
1331
2401
|
}
|
|
@@ -1353,7 +2423,34 @@ program
|
|
|
1353
2423
|
console.log(` The semantic invariant a linter can't see — run \`hunch conform\` for the full picture.`);
|
|
1354
2424
|
}
|
|
1355
2425
|
}
|
|
1356
|
-
|
|
2426
|
+
// CONSTITUTION POLICY: the same neutral receipt used by `hunch policy evaluate`
|
|
2427
|
+
// and hunch_policy_evaluate. Only an active_blocking policy with explicit human
|
|
2428
|
+
// authority can block. An evaluator error also fails strict CI; unknown remains
|
|
2429
|
+
// visible/advisory and can never masquerade as satisfied.
|
|
2430
|
+
const behavior = opts.working ? { workspace: "working" }
|
|
2431
|
+
: opts.commit ? { commit: revParse(opts.commit, root) }
|
|
2432
|
+
: opts.base ? undefined
|
|
2433
|
+
: { workspace: "staged" };
|
|
2434
|
+
const policyResults = hasActivePolicies
|
|
2435
|
+
? constitution.evaluate({ activeOnly: true, publicOnly: !!opts.publicOnly, behavior })
|
|
2436
|
+
: [];
|
|
2437
|
+
if (policyResults.length) {
|
|
2438
|
+
if (markdown) {
|
|
2439
|
+
console.log(`\n### 📜 Hunch Constitution — ${policyResults.length} policy receipt(s)\n`);
|
|
2440
|
+
for (const r of policyResults) {
|
|
2441
|
+
console.log(`- ${r.blocks ? "⛔" : r.evaluation.result === "satisfied" ? "✅" : r.evaluation.result === "error" ? "‼" : "⚠"} \`${r.policy.id}\` **${r.evaluation.result}** — ${r.evaluation.explanation}`);
|
|
2442
|
+
console.log(` - receipt: \`${r.evaluation.deterministic_hash}\`${r.blocks ? " **(authorized block)**" : ""}`);
|
|
2443
|
+
if (r.gate_error)
|
|
2444
|
+
console.log(` - ‼ gate error: ${r.gate_error}`);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
else {
|
|
2448
|
+
console.log("");
|
|
2449
|
+
renderPolicyEvaluations(policyResults).forEach((line) => console.log(line));
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
const constitutionFails = policyResults.some((r) => r.blocks || r.strict_error);
|
|
2453
|
+
if (reportFailsStrict(report) || (!!opts.strict && (confViolations.length > 0 || constitutionFails)))
|
|
1357
2454
|
process.exitCode = 1;
|
|
1358
2455
|
store.close();
|
|
1359
2456
|
});
|
|
@@ -1543,6 +2640,48 @@ program
|
|
|
1543
2640
|
const next = writeConfig(paths, { firmness: level }).firmness;
|
|
1544
2641
|
console.log(`✓ firmness set to ${next} (takes effect on the next agent edit — no restart needed).`);
|
|
1545
2642
|
});
|
|
2643
|
+
// ---- provider (per-user synthesis subscription choice) -------------------
|
|
2644
|
+
program
|
|
2645
|
+
.command("provider")
|
|
2646
|
+
.description("Show or set the local coding-assistant subscription Hunch may use for synthesis. Never changes team config.")
|
|
2647
|
+
.argument("[name]", `auto | ${SYNTH_PREFERENCES.filter((p) => p !== "auto").join(" | ")} (omit to inspect)`)
|
|
2648
|
+
.action(async (value) => {
|
|
2649
|
+
const root = findRoot();
|
|
2650
|
+
if (value != null) {
|
|
2651
|
+
const preference = value.trim();
|
|
2652
|
+
if (!SYNTH_PREFERENCES.includes(preference)) {
|
|
2653
|
+
return fail(`provider must be one of: ${SYNTH_PREFERENCES.join(", ")}`);
|
|
2654
|
+
}
|
|
2655
|
+
try {
|
|
2656
|
+
writeSynthesisPreference(root, preference);
|
|
2657
|
+
}
|
|
2658
|
+
catch (error) {
|
|
2659
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
2660
|
+
}
|
|
2661
|
+
console.log(`✓ local synthesis preference set to ${preference} (gitignored; it never changes a teammate's billing choice).`);
|
|
2662
|
+
}
|
|
2663
|
+
const resolution = await resolveSynthesisProvider({ root });
|
|
2664
|
+
const envValue = process.env.HUNCH_SYNTH_PROVIDER?.trim();
|
|
2665
|
+
const local = readSynthesisPreference(root);
|
|
2666
|
+
const hasValidEnv = !!envValue && SYNTH_PREFERENCES.includes(envValue);
|
|
2667
|
+
console.log(`selected: ${resolution.provider.name} (${resolution.source})`);
|
|
2668
|
+
console.log(`preference: ${hasValidEnv ? `environment: ${envValue}` : `local: ${local}`}`);
|
|
2669
|
+
if (envValue && !hasValidEnv)
|
|
2670
|
+
console.log(dim(`HUNCH_SYNTH_PROVIDER=${envValue} is unknown and is being ignored.`));
|
|
2671
|
+
console.log("available:");
|
|
2672
|
+
for (const status of resolution.statuses) {
|
|
2673
|
+
const billing = status.subscription ? ` — ${status.subscription}` : "";
|
|
2674
|
+
console.log(` ${status.available ? "✓" : "·"} ${status.name}: ${status.label}${billing}`);
|
|
2675
|
+
}
|
|
2676
|
+
if (resolution.source === "ambiguous") {
|
|
2677
|
+
const choices = resolution.statuses.filter((s) => s.name !== "deterministic" && s.available).map((s) => `hunch provider ${s.name}`);
|
|
2678
|
+
console.log(dim("Multiple subscription CLIs are available, so Hunch uses the free deterministic fallback rather than guessing which plan to spend."));
|
|
2679
|
+
console.log(`choose one: ${choices.join(" or ")}`);
|
|
2680
|
+
}
|
|
2681
|
+
else if (resolution.source === "unavailable-preference") {
|
|
2682
|
+
console.log(dim(`Your ${resolution.preference} preference is not available; Hunch is using the local deterministic fallback.`));
|
|
2683
|
+
}
|
|
2684
|
+
});
|
|
1546
2685
|
// ---- status (enforcement readiness at a glance) ---------------------------
|
|
1547
2686
|
program
|
|
1548
2687
|
.command("status")
|
|
@@ -1583,6 +2722,54 @@ program
|
|
|
1583
2722
|
store.close();
|
|
1584
2723
|
});
|
|
1585
2724
|
// ---- hook (multi-agent lifecycle hook handler) ----------------------------
|
|
2725
|
+
function parseStatsSince(value) {
|
|
2726
|
+
const match = /^(\d+)\s*([mhdw])$/.exec(value.trim());
|
|
2727
|
+
if (!match)
|
|
2728
|
+
return { ms: 7 * 864e5, label: "7d" };
|
|
2729
|
+
const units = { m: 6e4, h: 36e5, d: 864e5, w: 7 * 864e5 };
|
|
2730
|
+
const unit = match[2];
|
|
2731
|
+
return { ms: Number(match[1]) * units[unit], label: `${match[1]}${unit}` };
|
|
2732
|
+
}
|
|
2733
|
+
program
|
|
2734
|
+
.command("stats")
|
|
2735
|
+
.description("Compounding-value receipt: accumulated memory, caught violations, and payback")
|
|
2736
|
+
.option("--json", "emit the hunch.stats/1 machine contract")
|
|
2737
|
+
.option("--since <duration>", "recent return window (7d, 24h, 2w)", "7d")
|
|
2738
|
+
.option("--private", "include the local private/shared overlay")
|
|
2739
|
+
.action((opts) => {
|
|
2740
|
+
const { store, root } = storeFor();
|
|
2741
|
+
try {
|
|
2742
|
+
if (opts.private && !store.hasPrivate)
|
|
2743
|
+
return fail("--private needs a configured private/shared overlay");
|
|
2744
|
+
const load = (kind) => opts.private ? store.recs(kind) : store.json.loadAll(kind);
|
|
2745
|
+
const events = readEvents(hunchPaths(root));
|
|
2746
|
+
if (opts.private && store.privateDir)
|
|
2747
|
+
events.push(...readEvents(hunchPathsForDir(store.privateDir)));
|
|
2748
|
+
const since = parseStatsSince(opts.since);
|
|
2749
|
+
const now = Date.now();
|
|
2750
|
+
const constraints = load("constraints");
|
|
2751
|
+
const staleConstraints = constraints.filter((constraint) => {
|
|
2752
|
+
const verified = constraint.provenance.last_verified;
|
|
2753
|
+
if (!verified || Number.isNaN(Date.parse(verified)))
|
|
2754
|
+
return false;
|
|
2755
|
+
return constraint.scope.some((file) => {
|
|
2756
|
+
const changed = lastChangeDate(file, root);
|
|
2757
|
+
return !!changed && Date.parse(changed) > Date.parse(verified);
|
|
2758
|
+
});
|
|
2759
|
+
}).length;
|
|
2760
|
+
const stats = computeStats({
|
|
2761
|
+
decisions: load("decisions"), constraints, bugs: load("bugs"),
|
|
2762
|
+
componentIds: load("components").map((component) => component.id),
|
|
2763
|
+
runbooksCount: load("runbooks").length, events,
|
|
2764
|
+
staleConstraints,
|
|
2765
|
+
now, windowStart: now - since.ms, windowLabel: since.label,
|
|
2766
|
+
});
|
|
2767
|
+
console.log(opts.json ? JSON.stringify(stats, null, 2) : formatStats(stats));
|
|
2768
|
+
}
|
|
2769
|
+
finally {
|
|
2770
|
+
store.close();
|
|
2771
|
+
}
|
|
2772
|
+
});
|
|
1586
2773
|
program
|
|
1587
2774
|
.command("hook")
|
|
1588
2775
|
.description("Agent-agnostic hook handler: normalizes Claude, VS Code, Cursor, Windsurf, and Antigravity events into Hunch context and strict policy checks. Reads hook JSON on stdin.")
|
|
@@ -1723,6 +2910,7 @@ program
|
|
|
1723
2910
|
const proposedLines = proposedEditLines(evt.tool_input);
|
|
1724
2911
|
const deny = blockingInScope(store, target, proposedLines);
|
|
1725
2912
|
if (deny) {
|
|
2913
|
+
appendEvent(paths, { at: new Date().toISOString(), file: target, ...deny.event });
|
|
1726
2914
|
emitDeny(provider, deny.reason);
|
|
1727
2915
|
return;
|
|
1728
2916
|
}
|
|
@@ -1731,6 +2919,7 @@ program
|
|
|
1731
2919
|
// only human-confirmed tripwires deny.
|
|
1732
2920
|
const vetoDeny = proposedLines.length ? vetoInScope(store, target, proposedLines) : null;
|
|
1733
2921
|
if (vetoDeny) {
|
|
2922
|
+
appendEvent(paths, { at: new Date().toISOString(), file: target, ...vetoDeny.event });
|
|
1734
2923
|
emitDeny(provider, vetoDeny.reason);
|
|
1735
2924
|
return;
|
|
1736
2925
|
}
|
|
@@ -1852,9 +3041,18 @@ program
|
|
|
1852
3041
|
console.log(`✓ accepted ${opts.accept} (now ${source}, confidence 0.95${armed ? `, ${armed} tripwire(s) now blocking` : ""})`);
|
|
1853
3042
|
}
|
|
1854
3043
|
else if (opts.reject) {
|
|
1855
|
-
const
|
|
3044
|
+
const d = opts.private ? store.getRec("decisions", opts.reject) : store.json.get("decisions", opts.reject);
|
|
3045
|
+
if (!d) {
|
|
3046
|
+
store.close();
|
|
3047
|
+
return fail(`decision ${opts.reject} not found`);
|
|
3048
|
+
}
|
|
3049
|
+
if (d.status !== "proposed") {
|
|
3050
|
+
store.close();
|
|
3051
|
+
return fail(`refusing to reject ${d.status} decision ${d.id}; review --reject only removes proposed drafts`);
|
|
3052
|
+
}
|
|
3053
|
+
const ok2 = opts.private ? store.deleteWhereItLives("decisions", d.id) : store.json.delete("decisions", d.id);
|
|
1856
3054
|
store.reindex();
|
|
1857
|
-
console.log(ok2 ? `✓ rejected and removed ${
|
|
3055
|
+
console.log(ok2 ? `✓ rejected and removed ${d.id}` : `decision ${d.id} not found`);
|
|
1858
3056
|
}
|
|
1859
3057
|
else if (opts.rejectDuplicates) {
|
|
1860
3058
|
// Deterministic hygiene, not a trust decision (dec_a466655539 stays intact):
|
|
@@ -1941,8 +3139,8 @@ function printAutoEntry(e) {
|
|
|
1941
3139
|
}
|
|
1942
3140
|
program
|
|
1943
3141
|
.command("auto-review")
|
|
1944
|
-
.description("Harness-driven draft triage: delegate relevance to the coding-assistant CLI, then dedup, auto-confirm the verified+relevant, and delete duplicates/irrelevant. Dry-run unless --apply.")
|
|
1945
|
-
.option("--apply", "execute the plan (accept/delete). Without it, print the plan and change nothing.")
|
|
3142
|
+
.description("Harness-driven draft triage: delegate relevance to the coding-assistant CLI, then dedup, auto-confirm the verified+relevant, and delete duplicates/irrelevant. Dry-run unless --apply; apply refuses an incomplete requested harness batch.")
|
|
3143
|
+
.option("--apply", "execute the plan (accept/delete) only after a complete requested harness batch. Without it, print the plan and change nothing.")
|
|
1946
3144
|
.option("--min-grounded <n>", "grounded-ness threshold for the auto-accept gate", String(READY_MIN_GROUNDED))
|
|
1947
3145
|
.option("--min-reject-confidence <n>", "minimum harness confidence to DELETE an irrelevant draft (else kept for a human)", "0.7")
|
|
1948
3146
|
.option("--no-llm", "skip the harness judgment (dedup + grounding only — no relevance deletion)")
|
|
@@ -1961,28 +3159,37 @@ program
|
|
|
1961
3159
|
console.log("✓ No drafts to auto-review.");
|
|
1962
3160
|
return;
|
|
1963
3161
|
}
|
|
1964
|
-
// Delegate relevance to the harness (subscription CLI) — feature-detected
|
|
1965
|
-
//
|
|
3162
|
+
// Delegate relevance to the harness (subscription CLI) — feature-detected.
|
|
3163
|
+
// A dry-run may remain partial (missing verdicts are kept), but --apply is
|
|
3164
|
+
// all-or-nothing when judgment was requested: a provider outage must never
|
|
3165
|
+
// turn an incomplete batch into an apparently safe mutation plan.
|
|
1966
3166
|
const verdicts = new Map();
|
|
1967
|
-
|
|
1968
|
-
|
|
3167
|
+
const judgmentRequested = opts.llm !== false && !opts.private;
|
|
3168
|
+
const judgmentFailures = [];
|
|
3169
|
+
if (judgmentRequested) {
|
|
3170
|
+
const provider = await selectProvider({ root });
|
|
1969
3171
|
if (provider.judgeDraft) {
|
|
1970
|
-
//
|
|
3172
|
+
// Deletion anchors are only finalized, live, human-confirmed records.
|
|
1971
3173
|
const existing = all
|
|
1972
|
-
.filter(
|
|
3174
|
+
.filter(isAcceptedDuplicateAnchor)
|
|
1973
3175
|
.map((d) => ({ id: d.id, title: d.title, decision: d.decision }));
|
|
1974
3176
|
console.log(`Judging ${drafts.length} draft(s) via ${provider.name} (subscription)…`);
|
|
1975
3177
|
for (const d of drafts) {
|
|
1976
3178
|
try {
|
|
1977
3179
|
verdicts.set(d.id, await provider.judgeDraft(d, existing.filter((e) => e.id !== d.id)));
|
|
1978
3180
|
}
|
|
1979
|
-
catch {
|
|
1980
|
-
|
|
3181
|
+
catch (e) {
|
|
3182
|
+
// Preserve the safe keep-for-human plan while making the missing
|
|
3183
|
+
// evidence observable. The apply gate below binds to this exact
|
|
3184
|
+
// draft set, so partial provider success cannot mutate the store.
|
|
3185
|
+
const error = (e instanceof Error ? e.message : String(e)).replace(/\s+/g, " ").trim();
|
|
3186
|
+
judgmentFailures.push({ id: d.id, error: error.slice(0, 240) || "unknown provider failure" });
|
|
1981
3187
|
}
|
|
1982
3188
|
}
|
|
1983
3189
|
}
|
|
1984
3190
|
else {
|
|
1985
3191
|
console.log(dim("No subscription CLI available — relevance judgment skipped (dedup + grounding only)."));
|
|
3192
|
+
judgmentFailures.push(...drafts.map((d) => ({ id: d.id, error: "no subscription relevance judge available" })));
|
|
1986
3193
|
}
|
|
1987
3194
|
}
|
|
1988
3195
|
else if (opts.private && opts.llm !== false) {
|
|
@@ -1990,11 +3197,28 @@ program
|
|
|
1990
3197
|
}
|
|
1991
3198
|
const plan = planAutoReview(drafts, all, verdicts, { minGrounded, minRejectConfidence });
|
|
1992
3199
|
printAutoReviewPlan(plan);
|
|
3200
|
+
if (judgmentRequested) {
|
|
3201
|
+
const coverage = `${verdicts.size}/${drafts.length} judged`;
|
|
3202
|
+
if (judgmentFailures.length) {
|
|
3203
|
+
console.error(`\n⚠ Incomplete harness batch: ${coverage}; ${judgmentFailures.length} failure(s).`);
|
|
3204
|
+
for (const failure of judgmentFailures.slice(0, 5))
|
|
3205
|
+
console.error(` ${failure.id}: ${failure.error}`);
|
|
3206
|
+
if (judgmentFailures.length > 5)
|
|
3207
|
+
console.error(` … ${judgmentFailures.length - 5} more failure(s)`);
|
|
3208
|
+
}
|
|
3209
|
+
else {
|
|
3210
|
+
console.log(`\n✓ Harness batch complete: ${coverage}.`);
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
1993
3213
|
if (!opts.apply) {
|
|
1994
3214
|
const n = planMutations(plan);
|
|
1995
3215
|
console.log(`\n${dim(`Dry run — nothing changed. Re-run with --apply to ${n ? `apply ${n} change(s)` : "confirm (no changes)"}.`)}`);
|
|
1996
3216
|
return;
|
|
1997
3217
|
}
|
|
3218
|
+
if (judgmentRequested && judgmentFailures.length) {
|
|
3219
|
+
fail(`incomplete harness batch (${verdicts.size}/${drafts.length} judged); no changes applied. Re-run when the provider is healthy, or explicitly choose deterministic-only triage with --no-llm --apply.`);
|
|
3220
|
+
return;
|
|
3221
|
+
}
|
|
1998
3222
|
// Apply: accept the verified+relevant, delete duplicates + irrelevant.
|
|
1999
3223
|
let accepted = 0, deleted = 0, armedTotal = 0, publicAccepted = false;
|
|
2000
3224
|
for (const e of plan.accept) {
|
|
@@ -2284,7 +3508,7 @@ program
|
|
|
2284
3508
|
let prose;
|
|
2285
3509
|
let adoptionProse;
|
|
2286
3510
|
if (opts.llm !== false) {
|
|
2287
|
-
const provider = await selectProvider();
|
|
3511
|
+
const provider = await selectProvider({ root });
|
|
2288
3512
|
if (provider.draftProse) {
|
|
2289
3513
|
console.log(`Prose via ${provider.name} (subscription); the drift-bearing skeleton stays deterministic.`);
|
|
2290
3514
|
prose = (pack, excerpts) => provider.draftProse(wikiPrompt(pack, excerpts));
|
|
@@ -2516,27 +3740,37 @@ program
|
|
|
2516
3740
|
const onDisk = readManifest(hunchPaths(root)).schema_version;
|
|
2517
3741
|
const schemaNote = onDisk === SCHEMA_VERSION ? "" : onDisk > SCHEMA_VERSION ? ` ⚠ newer than this Hunch (v${SCHEMA_VERSION}) — upgrade hunch` : ` ⚠ run \`hunch migrate\``;
|
|
2518
3742
|
console.log(`schema: v${onDisk} (hunch v${SCHEMA_VERSION})${schemaNote}`);
|
|
2519
|
-
const
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
3743
|
+
const resolution = await resolveSynthesisProvider({ root });
|
|
3744
|
+
const provider = resolution.provider;
|
|
3745
|
+
console.log(`synthesis: ${provider.name} (${resolution.source})`);
|
|
3746
|
+
const selected = resolution.statuses.find((s) => s.name === provider.name);
|
|
3747
|
+
if (selected?.subscription) {
|
|
3748
|
+
console.log(` ↳ LLM synthesis uses your ${selected.subscription}; provider API credentials are not used.`);
|
|
3749
|
+
}
|
|
3750
|
+
else if (resolution.source === "ambiguous") {
|
|
3751
|
+
const names = resolution.statuses.filter((s) => s.name !== "deterministic" && s.available).map((s) => s.name);
|
|
3752
|
+
console.log(dim(` ↳ ${names.join(", ")} are available; Hunch will not guess which subscription to spend.`));
|
|
3753
|
+
console.log(dim(` choose one locally: ${names.map((name) => `hunch provider ${name}`).join(" or ")}`));
|
|
3754
|
+
}
|
|
3755
|
+
else if (resolution.source === "unavailable-preference") {
|
|
3756
|
+
console.log(dim(` ↳ ${resolution.preference} was selected but is unavailable; using the offline heuristic.`));
|
|
2533
3757
|
}
|
|
2534
3758
|
else {
|
|
2535
|
-
console.log(dim(` ↳ no assistant CLI found — synthesis uses the offline heuristic (advisory, low-confidence)
|
|
2536
|
-
console.log(dim(`
|
|
3759
|
+
console.log(dim(` ↳ no assistant CLI found — synthesis uses the offline heuristic (advisory, low-confidence).`));
|
|
3760
|
+
console.log(dim(` install or log into Claude Code, Codex, or Cursor; then select one with \`hunch provider <name>\`.`));
|
|
2537
3761
|
}
|
|
2538
3762
|
const c = store.reindex().counts;
|
|
2539
3763
|
console.log(`hunch: ${c.symbols} symbols, ${c.edges} edges, ${c.components} components, ${c.decisions} decisions, ${c.bugs} bugs, ${c.constraints} constraints`);
|
|
3764
|
+
try {
|
|
3765
|
+
const policies = new ConstitutionService(store, root).list();
|
|
3766
|
+
const active = policies.filter((p) => p.state === "active_advisory" || p.state === "active_blocking").length;
|
|
3767
|
+
const proposed = policies.filter((p) => p.state === "proposed").length;
|
|
3768
|
+
console.log(`constitution: ${policies.length} policies (${active} active, ${proposed} proposed)`);
|
|
3769
|
+
}
|
|
3770
|
+
catch (e) {
|
|
3771
|
+
console.log(`constitution: ⛔ ${e.message}`);
|
|
3772
|
+
process.exitCode = 1;
|
|
3773
|
+
}
|
|
2540
3774
|
// Overlay status speaks the TRUE mode, and a dead pointer is a loud finding, not a
|
|
2541
3775
|
// silent empty store: the JSON reader degrades to [] when the target dir is missing,
|
|
2542
3776
|
// so this is the one place the loss is visible.
|