@davesheffer/hunch 1.31.0 → 1.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -6
- package/dist/cli/automaticReviewMemory.d.ts +13 -0
- package/dist/cli/dna.d.ts +2 -0
- package/dist/cli/index.d.ts +16 -0
- package/dist/cli/index.js +602 -43
- package/dist/cli/integrations.d.ts +2 -0
- package/dist/cli/integrations.js +5 -2
- package/dist/cli/invocation.d.ts +35 -0
- package/dist/cli/preflight.d.ts +1 -0
- package/dist/cli/reviewMemory.d.ts +4 -0
- package/dist/cli/reviewMemoryProvider.d.ts +14 -0
- package/dist/cli/serve.d.ts +2 -0
- package/dist/cli/taskReport.d.ts +6 -0
- package/dist/cli/taskReport.js +124 -0
- package/dist/cli/update.d.ts +14 -0
- package/dist/client/state.d.ts +248 -0
- package/dist/client/state.js +2 -0
- package/dist/constitution/adapters.d.ts +29 -0
- package/dist/constitution/behaviorAttestationBinding.d.ts +3 -0
- package/dist/constitution/behaviorEvaluator.d.ts +8 -0
- package/dist/constitution/behaviorProof.d.ts +9 -0
- package/dist/constitution/behaviorWorkspace.d.ts +25 -0
- package/dist/constitution/bootstrap.d.ts +30 -0
- package/dist/constitution/canonical.d.ts +15 -0
- package/dist/constitution/card.d.ts +58 -0
- package/dist/constitution/compiler.d.ts +52 -0
- package/dist/constitution/composition.d.ts +10 -0
- package/dist/constitution/corpus.d.ts +7 -0
- package/dist/constitution/correctionPolicyMaterializer.d.ts +57 -0
- package/dist/constitution/delta.d.ts +4 -0
- package/dist/constitution/disposition.d.ts +20 -0
- package/dist/constitution/evaluator.d.ts +47 -0
- package/dist/constitution/experiment.d.ts +466 -0
- package/dist/constitution/experimentRunner.d.ts +7 -0
- package/dist/constitution/g2.d.ts +209 -0
- package/dist/constitution/g2BehaviorAttestation.d.ts +45 -0
- package/dist/constitution/g2BehaviorCandidates.d.ts +123 -0
- package/dist/constitution/g2BehaviorDependencies.d.ts +83 -0
- package/dist/constitution/g2BehaviorMaterialization.d.ts +77 -0
- package/dist/constitution/g2BehaviorPolicyMaterializer.d.ts +48 -0
- package/dist/constitution/g2CandidateAttestation.d.ts +43 -0
- package/dist/constitution/g2Candidates.d.ts +80 -0
- package/dist/constitution/g2Drills.d.ts +33 -0
- package/dist/constitution/g3.d.ts +311 -0
- package/dist/constitution/g3Conformance.d.ts +33 -0
- package/dist/constitution/lifecycle.d.ts +25 -0
- package/dist/constitution/mutation.d.ts +15 -0
- package/dist/constitution/nodeTestEvidence.d.ts +13 -0
- package/dist/constitution/plan.d.ts +18 -0
- package/dist/constitution/policyRuntime.d.ts +5 -0
- package/dist/constitution/proof.d.ts +11 -0
- package/dist/constitution/repairPolicies.d.ts +34 -0
- package/dist/constitution/replacementFreeGit.d.ts +14 -0
- package/dist/constitution/replay.d.ts +40 -0
- package/dist/constitution/replayCache.d.ts +14 -0
- package/dist/constitution/replayWorker.d.ts +1 -0
- package/dist/constitution/repository.d.ts +129 -0
- package/dist/constitution/safeCheckout.d.ts +9 -0
- package/dist/constitution/schema.d.ts +1763 -0
- package/dist/constitution/scorecard.d.ts +219 -0
- package/dist/constitution/service.d.ts +398 -0
- package/dist/constitution/shadow.d.ts +54 -0
- package/dist/constitution/sourceMutation.d.ts +15 -0
- package/dist/constitution/staticGraphBaseline.d.ts +13 -0
- package/dist/constitution/structural.d.ts +64 -0
- package/dist/core/agenthook.d.ts +63 -0
- package/dist/core/agenthook.js +1 -0
- package/dist/core/automaticReviewMemory.d.ts +60 -0
- package/dist/core/autoreview.d.ts +66 -0
- package/dist/core/canonicalOrder.d.ts +3 -0
- package/dist/core/capturetoken.d.ts +19 -0
- package/dist/core/changeIdentity.d.ts +22 -0
- package/dist/core/changeProof.d.ts +14 -0
- package/dist/core/checkreport.d.ts +140 -0
- package/dist/core/commitrepair.d.ts +171 -0
- package/dist/core/commitrepair.js +238 -0
- package/dist/core/compare.d.ts +21 -0
- package/dist/core/config.d.ts +20 -0
- package/dist/core/conformance.d.ts +35 -0
- package/dist/core/constraintmatch.d.ts +54 -0
- package/dist/core/correction.d.ts +42 -0
- package/dist/core/correctionStage.d.ts +224 -0
- package/dist/core/declarationClusters.d.ts +149 -0
- package/dist/core/delivery.d.ts +110 -0
- package/dist/core/docanchors.d.ts +32 -0
- package/dist/core/docscan.d.ts +26 -0
- package/dist/core/drift.d.ts +13 -0
- package/dist/core/drift.js +31 -1
- package/dist/core/dupdetect.d.ts +43 -0
- package/dist/core/escalations.d.ts +129 -0
- package/dist/core/escalations.js +165 -0
- package/dist/core/events.d.ts +29 -0
- package/dist/core/evidenceMap.d.ts +164 -0
- package/dist/core/externalImports.d.ts +8 -0
- package/dist/core/format.d.ts +13 -0
- package/dist/core/glob.d.ts +9 -0
- package/dist/core/groundingLag.d.ts +84 -0
- package/dist/core/groundingLag.js +16 -2
- package/dist/core/groundingMerge.d.ts +15 -0
- package/dist/core/groundingMerge.js +95 -0
- package/dist/core/hookcache.d.ts +10 -0
- package/dist/core/hookpolicy.d.ts +47 -0
- package/dist/core/ids.d.ts +36 -0
- package/dist/core/importReview.d.ts +30 -0
- package/dist/core/io.d.ts +23 -0
- package/dist/core/jsonc.d.ts +4 -0
- package/dist/core/landscapeAdoption.d.ts +66 -0
- package/dist/core/landscapeDelivery.d.ts +76 -0
- package/dist/core/memorylog.d.ts +37 -0
- package/dist/core/migrate.d.ts +28 -0
- package/dist/core/outcomeExperience.d.ts +89 -0
- package/dist/core/overlaySafety.d.ts +24 -0
- package/dist/core/overlaySafety.js +7 -1
- package/dist/core/paths.d.ts +31 -0
- package/dist/core/pipeline.d.ts +289 -0
- package/dist/core/premises.d.ts +43 -0
- package/dist/core/provenance.d.ts +21 -0
- package/dist/core/publication.d.ts +54 -0
- package/dist/core/refrepair.d.ts +24 -0
- package/dist/core/refrepair.js +1 -1
- package/dist/core/relativeImports.d.ts +11 -0
- package/dist/core/repair.d.ts +44 -0
- package/dist/core/repairqueue.d.ts +40 -0
- package/dist/core/repairqueue.js +133 -0
- package/dist/core/reviewMemory.d.ts +40 -0
- package/dist/core/reviewqueue.d.ts +54 -0
- package/dist/core/safeRepoFile.d.ts +20 -0
- package/dist/core/served.d.ts +57 -0
- package/dist/core/served.js +14 -0
- package/dist/core/stateContract.d.ts +979 -0
- package/dist/core/stateContract.js +87 -4
- package/dist/core/stateDelivery.d.ts +78 -0
- package/dist/core/stateRecords.d.ts +383 -0
- package/dist/core/stateRecords.js +16 -1
- package/dist/core/stats.d.ts +88 -0
- package/dist/core/strictgate.d.ts +33 -0
- package/dist/core/taskReport.d.ts +226 -0
- package/dist/core/taskReport.js +514 -0
- package/dist/core/taskReportCapture.d.ts +8 -0
- package/dist/core/taskReportCapture.js +36 -0
- package/dist/core/taskReportEvidence.d.ts +26 -0
- package/dist/core/taskReportEvidence.js +268 -0
- package/dist/core/taskReportHook.d.ts +20 -0
- package/dist/core/taskReportHook.js +76 -0
- package/dist/core/taskReportPaths.d.ts +3 -0
- package/dist/core/taskReportPaths.js +23 -0
- package/dist/core/taskReportPublic.d.ts +31 -0
- package/dist/core/taskReportPublic.js +45 -0
- package/dist/core/taskReportRender.d.ts +6 -0
- package/dist/core/taskReportRender.js +97 -0
- package/dist/core/topics.d.ts +73 -0
- package/dist/core/types.d.ts +1239 -0
- package/dist/core/version.d.ts +10 -0
- package/dist/eval/guards.d.ts +45 -0
- package/dist/eval/harness.d.ts +58 -0
- package/dist/extractors/adrImport.d.ts +59 -0
- package/dist/extractors/comments.d.ts +7 -0
- package/dist/extractors/correctionSources.d.ts +11 -0
- package/dist/extractors/diff.d.ts +40 -0
- package/dist/extractors/git.d.ts +304 -0
- package/dist/extractors/git.js +208 -20
- package/dist/extractors/helm.d.ts +31 -0
- package/dist/extractors/indexer.d.ts +55 -0
- package/dist/extractors/landscapeDiscovery.d.ts +41 -0
- package/dist/extractors/languages.d.ts +77 -0
- package/dist/extractors/nativeTreeSitter.d.ts +16 -0
- package/dist/extractors/parse.d.ts +54 -0
- package/dist/extractors/php.d.ts +26 -0
- package/dist/extractors/repoSource.d.ts +53 -0
- package/dist/extractors/testreport.d.ts +36 -0
- package/dist/integrations/ciAction.d.ts +8 -0
- package/dist/integrations/claudeConfig.d.ts +44 -0
- package/dist/integrations/claudemd.d.ts +13 -0
- package/dist/integrations/claudemd.js +11 -2
- package/dist/integrations/gitignore.d.ts +16 -0
- package/dist/integrations/gitignore.js +7 -0
- package/dist/integrations/health.d.ts +74 -0
- package/dist/integrations/hooks.d.ts +41 -0
- package/dist/integrations/hooks.js +92 -66
- package/dist/integrations/madrExport.d.ts +39 -0
- package/dist/integrations/madrManifest.d.ts +69 -0
- package/dist/integrations/mergeDriver.d.ts +3 -0
- package/dist/integrations/mergeDriver.js +25 -8
- package/dist/integrations/probe.d.ts +4 -0
- package/dist/integrations/providers.d.ts +103 -0
- package/dist/integrations/scaffold.d.ts +29 -0
- package/dist/integrations/sync.d.ts +30 -0
- package/dist/integrations/sync.js +3 -2
- package/dist/integrations/team.d.ts +70 -0
- package/dist/integrations/worktree.d.ts +8 -0
- package/dist/mcp/roots.d.ts +16 -0
- package/dist/mcp/server.d.ts +36 -0
- package/dist/mcp/server.js +114 -25
- package/dist/mcp/taskReportTools.d.ts +3 -0
- package/dist/mcp/taskReportTools.js +106 -0
- package/dist/serve/app.d.ts +36 -0
- package/dist/serve/app.js +20 -0
- package/dist/serve/config.d.ts +100 -0
- package/dist/serve/writelock.d.ts +22 -0
- package/dist/store/changeLedger.d.ts +132 -0
- package/dist/store/changeLedger.js +40 -9
- package/dist/store/compact.d.ts +33 -0
- package/dist/store/db.d.ts +9 -0
- package/dist/store/embedder.d.ts +32 -0
- package/dist/store/hunchStore.d.ts +624 -0
- package/dist/store/hunchStore.js +13 -5
- package/dist/store/jsonStore.d.ts +125 -0
- package/dist/store/jsonStore.js +15 -0
- package/dist/store/merge.d.ts +39 -0
- package/dist/store/privateMigrate.d.ts +24 -0
- package/dist/store/replay.d.ts +56 -0
- package/dist/store/schema.d.ts +17 -0
- package/dist/store/stateBinding.d.ts +145 -0
- package/dist/store/stateBinding.js +110 -15
- package/dist/store/stateCapture.d.ts +15 -0
- package/dist/store/stateCapture.js +145 -0
- package/dist/synthesis/cliAdapter.d.ts +22 -0
- package/dist/synthesis/initiator.d.ts +13 -0
- package/dist/synthesis/provider.d.ts +307 -0
- package/dist/synthesis/synthesize.d.ts +117 -0
- package/dist/synthesis/tripwires.d.ts +9 -0
- package/dist/taskReports.d.ts +76 -0
- package/dist/taskReports.js +67 -0
- package/dist/wiki/adopt.d.ts +22 -0
- package/dist/wiki/graph.d.ts +87 -0
- package/dist/wiki/wiki.d.ts +242 -0
- package/package.json +6 -5
- package/server.json +2 -2
package/dist/mcp/server.js
CHANGED
|
@@ -14,6 +14,8 @@ import { hunchPaths, findRoot, toPosixTarget } from "../core/paths.js";
|
|
|
14
14
|
import { canonicalRootPath, resolveActiveRoot } from "./roots.js";
|
|
15
15
|
import { HunchStore } from "../store/hunchStore.js";
|
|
16
16
|
import { StateRefusal, SubscribeResponseSchema, capabilities, partitionOf, readState, recordsState, subscribeState, writeState } from "../store/stateBinding.js";
|
|
17
|
+
import { captureState, captureBatchState } from "../store/stateCapture.js";
|
|
18
|
+
import { CaptureRequestSchema, CaptureBatchRequestSchema, CaptureBatchResultSchema, STATE_CAPTURE_VERSION, STATE_CAPTURE_BATCH_VERSION } from "../core/stateContract.js";
|
|
17
19
|
import { ReadRequestSchema, ReadResponseSchema, WriteRequestSchema, WriteResultSchema, SubscribeRequestSchema, RecordsRequestSchema, RecordsResponseSchema, STATE_READ_VERSION, STATE_WRITE_VERSION, STATE_SUBSCRIBE_VERSION, STATE_RECORDS_VERSION, stateHash } from "../core/stateContract.js";
|
|
18
20
|
import { selectEmbedder } from "../store/embedder.js";
|
|
19
21
|
import { decisionId, findingId } from "../core/ids.js";
|
|
@@ -38,6 +40,10 @@ import { PROJECT_DNA_DELTA_SCHEMA_VERSION, diffProjectDna } from "../core/projec
|
|
|
38
40
|
import { projectDnaDeliverySupplement } from "../core/projectDnaDelivery.js";
|
|
39
41
|
import { armExecutionObligations, loadPipelineState, savePipelineState } from "../core/pipeline.js";
|
|
40
42
|
import { recordServed } from "../core/served.js";
|
|
43
|
+
import { TaskIdSchema, recordTaskDelivery } from "../core/taskReport.js";
|
|
44
|
+
import { observeReportCapture } from "../core/taskReportCapture.js";
|
|
45
|
+
import { snapshotDeliveredRecords } from "../core/taskReportEvidence.js";
|
|
46
|
+
import { registerTaskReportTools } from "./taskReportTools.js";
|
|
41
47
|
import { EdgeSchema, ResourceSchema } from "../core/types.js";
|
|
42
48
|
import { compareCandidates } from "../core/compare.js";
|
|
43
49
|
import { checkConformance } from "../core/conformance.js";
|
|
@@ -49,7 +55,8 @@ import { nowData, wikiStatus, publicHome, readWikiManifestAt } from "../wiki/wik
|
|
|
49
55
|
import { HUNCH_VERSION } from "../core/version.js";
|
|
50
56
|
import { assertCompleteRepoScan, indexRepo, scanRepo } from "../extractors/indexer.js";
|
|
51
57
|
import { liveForTopic, historyForTopic, rejectedForTopic, captureConflicts } from "../core/topics.js";
|
|
52
|
-
import { pendingEscalations, policyEscalations } from "../core/escalations.js";
|
|
58
|
+
import { pendingEscalations, policyEscalations, commitRepairEscalations, actionableEscalations, escalationHeadline } from "../core/escalations.js";
|
|
59
|
+
import { readActivePendingRepairs, withheldRewrites } from "../core/repairqueue.js";
|
|
53
60
|
import { scanRecord, publicationWarning, loadVocabulary } from "../core/publication.js";
|
|
54
61
|
import { premiseEscalations } from "../core/premises.js";
|
|
55
62
|
import { applyImportedAdrReview, pendingImportedAdrReviews } from "../core/importReview.js";
|
|
@@ -768,6 +775,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
768
775
|
});
|
|
769
776
|
return ok(`Top matches for "${query}":\n\n${lines.join("\n")}`);
|
|
770
777
|
});
|
|
778
|
+
registerTaskReportTools(server, () => root, () => store);
|
|
771
779
|
// -- hunch_runbook --------------------------------------------------------
|
|
772
780
|
server.registerTool("hunch_runbook", {
|
|
773
781
|
title: "Find a runbook for a task",
|
|
@@ -1030,9 +1038,26 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1030
1038
|
budget_tokens: z.number().optional().describe("Rough token budget for the brief (default 1500)."),
|
|
1031
1039
|
profile: z.enum(DELIVERY_PROFILES).optional().describe("Delivery role: builder (default), reviewer, or architect. Changes non-blocking order only."),
|
|
1032
1040
|
as_of: z.string().optional().describe("Time-travel ref (commit/tag/branch): assemble the slice as it stood then."),
|
|
1041
|
+
task_id: TaskIdSchema.optional().describe("Exact task ID from hunch_task; records this delivery for the task's contribution report."),
|
|
1042
|
+
cwd: cwdHintField,
|
|
1033
1043
|
},
|
|
1034
1044
|
outputSchema: DELIVERY_OUTPUT_SCHEMA,
|
|
1035
|
-
}, async ({ target, budget_tokens, profile, as_of }, extra) => {
|
|
1045
|
+
}, async ({ target, budget_tokens, profile, as_of, task_id }, extra) => {
|
|
1046
|
+
const deliver = (envelope) => {
|
|
1047
|
+
const result = deliveredContext(root, as_of ? `${target} (as_of:${as_of})` : target, envelope, extra.sessionId);
|
|
1048
|
+
if (task_id) {
|
|
1049
|
+
try {
|
|
1050
|
+
// Historical contexts must not borrow today's record text/revision.
|
|
1051
|
+
const records = as_of ? [] : snapshotDeliveredRecords(store, envelope);
|
|
1052
|
+
const occurrence = recordTaskDelivery(root, task_id, envelope, records);
|
|
1053
|
+
result.content.push({ type: "text", text: `Task evidence: ${task_id} · occurrence ${occurrence}.\n${records.slice(0, 20).map(r => `${r.record_id} @ ${r.content_hash}`).join("\n")}${records.length > 20 ? "\nMore record identities: hunch_report(task_id)." : ""}` });
|
|
1054
|
+
}
|
|
1055
|
+
catch {
|
|
1056
|
+
result.content.push({ type: "text", text: `Task evidence could not be recorded for ${task_id}. Context remains available; this delivery's report attribution is unverified. Check the task ID, working directory, and local ledger.` });
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return result;
|
|
1060
|
+
};
|
|
1036
1061
|
const asOf = as_of ? asOfDate(as_of, root) : undefined;
|
|
1037
1062
|
if (as_of && !asOf)
|
|
1038
1063
|
return invalid(`Could not resolve as_of "${as_of}" to a commit.`);
|
|
@@ -1096,11 +1121,11 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1096
1121
|
})),
|
|
1097
1122
|
],
|
|
1098
1123
|
});
|
|
1099
|
-
return
|
|
1124
|
+
return deliver(envelope);
|
|
1100
1125
|
}
|
|
1101
1126
|
}
|
|
1102
1127
|
const envelope = buildDeliveryEnvelope(ctx, options);
|
|
1103
|
-
return
|
|
1128
|
+
return deliver(envelope);
|
|
1104
1129
|
});
|
|
1105
1130
|
// -- hunch_shortlist (bounded correction-stage diagnostic) ----------------
|
|
1106
1131
|
server.registerTool("hunch_shortlist", {
|
|
@@ -1144,10 +1169,13 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1144
1169
|
// -- hunch_now (the hot view: recent activity + roadmap) --------------------
|
|
1145
1170
|
// PUBLIC store only, per dec_29eff08c69's jurisdiction rule: an assistant may
|
|
1146
1171
|
// paste this anywhere, so it must be publishable by construction. Union view
|
|
1147
|
-
// stays behind `hunch now --private` on the local terminal.
|
|
1172
|
+
// stays behind `hunch now --private` on the local terminal. EXCEPTION: a
|
|
1173
|
+
// queued commit-repair's liveness is checked against the full store (see
|
|
1174
|
+
// below), so a private-overlay decision's id and commit shas — never its
|
|
1175
|
+
// title — can surface in the escalation line.
|
|
1148
1176
|
server.registerTool("hunch_now", {
|
|
1149
1177
|
title: "Recent activity + the roadmap (the hot view)",
|
|
1150
|
-
description: "What just happened and what's next, straight from the graph: the last N decisions, the ROADMAP, and any inline human question such as an imported ADR awaiting explicit approve/decline. Call at session start to orient, or before planning what to work on. Same data as the wiki's now.md. Public store only.",
|
|
1178
|
+
description: "What just happened and what's next, straight from the graph: the last N decisions, the ROADMAP, and any inline human question such as an imported ADR awaiting explicit approve/decline. Call at session start to orient, or before planning what to work on. Same data as the wiki's now.md. Public store only, EXCEPT a queued commit-repair's liveness is checked against the full store (so a private-overlay decision's fully-answerable repair doesn't go silently unanswerable); only its id and the commit shas ever surface, never its title.",
|
|
1151
1179
|
inputSchema: {
|
|
1152
1180
|
recent_limit: z.number().optional().describe("How many recent decisions to include (default 10)."),
|
|
1153
1181
|
},
|
|
@@ -1165,8 +1193,18 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1165
1193
|
L.push("", `${pendingReview} legacy un-vouched draft(s) — \`hunch adopt-drafts\` auto-trusts them as advisory (new captures land trusted automatically).`);
|
|
1166
1194
|
const escalations = pendingEscalations(store.advisoryRecs("decisions"));
|
|
1167
1195
|
escalations.push(...premiseEscalations(store.advisoryRecs("decisions"), { now: new Date().toISOString(), exists: (p) => existsSync(join(root, p)) }));
|
|
1168
|
-
|
|
1169
|
-
|
|
1196
|
+
// liveness checked against the full store (repair-provenance reads the
|
|
1197
|
+
// full store too) even though the title stays scoped to advisoryRecs —
|
|
1198
|
+
// an overlay decision's repair is fully answerable, so it must not go
|
|
1199
|
+
// silent here just because its title is private.
|
|
1200
|
+
const hunchNowQueue = readActivePendingRepairs(root);
|
|
1201
|
+
escalations.push(...commitRepairEscalations(hunchNowQueue, store.advisoryRecs("decisions"), store.recs("decisions"), withheldRewrites(root, hunchNowQueue)));
|
|
1202
|
+
// Only ACTIONABLE entries are a question the assistant can put to the human
|
|
1203
|
+
// directly — a duplicate-id commit-repair follower whose own resolution
|
|
1204
|
+
// says "act on a different entry first" isn't one (#61).
|
|
1205
|
+
const actionableNow = actionableEscalations(escalations);
|
|
1206
|
+
if (actionableNow.length) {
|
|
1207
|
+
L.push("", `⚖ ${actionableNow.length} decision(s) need the human's call — ASK inline (never queue): ${actionableNow.map((e) => e.question).join(" · ")}`);
|
|
1170
1208
|
}
|
|
1171
1209
|
return ok(L.join("\n"));
|
|
1172
1210
|
});
|
|
@@ -1179,22 +1217,32 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1179
1217
|
// (con_e04226bd05): no Claude-specific behavior.
|
|
1180
1218
|
server.registerTool("hunch_escalations", {
|
|
1181
1219
|
title: "Decisions the human must make now (ask inline, not a queue)",
|
|
1182
|
-
description: "The rare decisions the graph cannot resolve on its own — surfaced so you ASK THE USER in the prompt at the moment, then act.
|
|
1220
|
+
description: "The rare decisions the graph cannot resolve on its own — surfaced so you ASK THE USER in the prompt at the moment, then act. Auto-captured memory is trusted automatically and never appears here; this returns topic conflicts (>1 live decision for one topic), premise-stale decisions (a live decision whose recorded REASON no longer holds — its authority is unchanged until the human re-attests, supersedes, or retires), one exact imported ADR at a time awaiting approve/decline, a queued commit-provenance repair (the post-merge hook detected a decision's commit was squash-merged away but never applies the fix unattended — apply with `hunch repair-provenance --apply` or leave it queued), and Constitution human moments (candidate policies awaiting review, proposed policies awaiting an activation decision). Normally empty. Raise each question with the user; do NOT decide it for them — an entry is a question, silence is never approval. Reads the public store, or the unified overlay when the repo is in shared mode — never private-mode overlay records, EXCEPT a queued commit-repair, whose liveness is checked against the full store; only its id and commit shas ever surface, never its title.",
|
|
1183
1221
|
inputSchema: {},
|
|
1184
1222
|
}, async () => {
|
|
1185
1223
|
const items = pendingEscalations(store.advisoryRecs("decisions"));
|
|
1186
1224
|
// Premise decay: a live decision whose recorded reason died. Question-framed
|
|
1187
1225
|
// like every entry here — authority never changes until the human answers.
|
|
1188
1226
|
items.push(...premiseEscalations(store.advisoryRecs("decisions"), { now: new Date().toISOString(), exists: (p) => existsSync(join(root, p)) }));
|
|
1227
|
+
// See the sibling hunch_now handler above for why `live` is the full store.
|
|
1228
|
+
const hunchEscalationsQueue = readActivePendingRepairs(root);
|
|
1229
|
+
items.push(...commitRepairEscalations(hunchEscalationsQueue, store.advisoryRecs("decisions"), store.recs("decisions"), withheldRewrites(root, hunchEscalationsQueue)));
|
|
1189
1230
|
try {
|
|
1190
1231
|
items.push(...policyEscalations(new ConstitutionService(store, root).list({ publicOnly: true }).map((p) => ({ ...p, last_action: p.audit.at(-1)?.action ?? null }))));
|
|
1191
1232
|
}
|
|
1192
1233
|
catch { /* constitution unavailable — memory escalations still surface */ }
|
|
1193
1234
|
if (!items.length)
|
|
1194
1235
|
return ok("✓ Nothing needs a human decision — memory is auto-trusted and self-consistent.");
|
|
1195
|
-
|
|
1236
|
+
// Only ACTIONABLE entries are a question to ask the human directly — a
|
|
1237
|
+
// duplicate-id commit-repair follower whose own resolution says "act on
|
|
1238
|
+
// a different entry first" still surfaces below for transparency, but
|
|
1239
|
+
// isn't itself something to raise as a decision (#61).
|
|
1240
|
+
const L = [escalationHeadline(items, "mcp"), ""];
|
|
1196
1241
|
for (const e of items) {
|
|
1197
|
-
|
|
1242
|
+
// Marked distinctly (never the "⚖ question" glyph) when it isn't
|
|
1243
|
+
// itself something to raise — an entry whose own answer is "act on a
|
|
1244
|
+
// different entry first" reads as informational, not as a question.
|
|
1245
|
+
L.push(`${e.actionable === false ? "·" : "⚖"} ${e.question}`);
|
|
1198
1246
|
L.push(` ${e.detail}`);
|
|
1199
1247
|
L.push(` → ${e.resolution}`, "");
|
|
1200
1248
|
}
|
|
@@ -1369,9 +1417,10 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1369
1417
|
private: z.boolean().optional().describe("write into the PRIVATE overlay store (HUNCH_PRIVATE_DIR) instead of the committed repo — for sensitive decisions kept out of a public repo. Errors if no private store is configured."),
|
|
1370
1418
|
}),
|
|
1371
1419
|
capture_token: z.string().optional().describe("token from hunch_capture_decision — proves this write is the tail of a grilling interview. Omit only for a quick manual record (a deprecation nudge is returned)."),
|
|
1420
|
+
task_id: TaskIdSchema.optional().describe("Exact task ID for observing this successful save; reporting never changes capture authority."),
|
|
1372
1421
|
cwd: cwdHintField,
|
|
1373
1422
|
},
|
|
1374
|
-
}, async ({ decision, capture_token }) => {
|
|
1423
|
+
}, async ({ decision, capture_token, task_id }) => {
|
|
1375
1424
|
try {
|
|
1376
1425
|
// Commit-keyed on the CANONICAL full sha (resolved via git rev-parse), so a
|
|
1377
1426
|
// human passing the short sha they see in `commit` produces the SAME id as
|
|
@@ -1508,7 +1557,8 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1508
1557
|
// closes the other, leaving two live decisions on one topic and grounding
|
|
1509
1558
|
// silently injecting nothing for it. The guard throws; the surrounding catch
|
|
1510
1559
|
// turns that into a clean tool error instead of a silent twin.
|
|
1511
|
-
store.putCapture("decisions", rec, !!decision.private);
|
|
1560
|
+
const stored = store.putCapture("decisions", rec, !!decision.private);
|
|
1561
|
+
const observed = observeReportCapture(root, task_id, "decisions", stored, home, !!existing, home === "private" ? store.privateDir ?? undefined : hunchPaths(root).hunch);
|
|
1512
1562
|
// Invalidate, don't delete: closing the superseded decision's valid-time window
|
|
1513
1563
|
// (+ a supersedes edge) preserves the why-it-changed trail. Route the close to the
|
|
1514
1564
|
// same store the new record landed in — a private decision supersedes within the
|
|
@@ -1521,8 +1571,8 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1521
1571
|
// Auto-flush the store the record landed in (on by default in every mode): a private
|
|
1522
1572
|
// record commits+pushes its overlay repo; a public one commits .hunch/ in THIS repo
|
|
1523
1573
|
// (commit only — it rides the user's next push, never auto-pushing their code branch).
|
|
1524
|
-
const flush = flushCapture(store, hunchPaths(root).hunch, !!decision.private, `hunch: capture ${id}`, startupTeamRoute ?? undefined);
|
|
1525
|
-
const flushed = flushNote(flush, home, store.mode) + publicHomeNote(home, store.hasPrivate, rec, hunchPaths(root).hunch);
|
|
1574
|
+
const flush = flushCapture(store, hunchPaths(root).hunch, !!decision.private, `hunch: capture ${id}`, startupTeamRoute ?? undefined, observed.observe);
|
|
1575
|
+
const flushed = flushNote(flush, home, store.mode) + publicHomeNote(home, store.hasPrivate, rec, hunchPaths(root).hunch) + observed.note;
|
|
1526
1576
|
// Capture-session gate (staged deprecation, §9.3): the token was consumed
|
|
1527
1577
|
// above (it also decides the provenance tier). No token still writes
|
|
1528
1578
|
// (non-breaking) but lands as agent_recorded with a nudge toward /capture.
|
|
@@ -1569,6 +1619,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1569
1619
|
source_decision: z.string().optional().describe("id of a decision this correction derives from."),
|
|
1570
1620
|
private: z.boolean().optional().describe("write into the PRIVATE overlay store (HUNCH_PRIVATE_DIR) instead of the committed repo — a sensitive rule enforced locally (pre-edit hook + local check) but never exposed in a public PR comment. Errors if no private store is configured."),
|
|
1571
1621
|
capture_token: z.string().optional().describe("token from hunch_capture_decision. The rule is recorded and enforced either way — the token only decides whether it may DENY: without one it lands as advisory testimony capped at severity 'warning'."),
|
|
1622
|
+
task_id: TaskIdSchema.optional().describe("Exact task ID for observing this successful save; reporting never changes capture authority."),
|
|
1572
1623
|
cwd: cwdHintField,
|
|
1573
1624
|
},
|
|
1574
1625
|
}, async (input) => {
|
|
@@ -1594,7 +1645,8 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1594
1645
|
}
|
|
1595
1646
|
const existing = home === "private" ? store.getPrivateRec("constraints", rec.id) : store.json.get("constraints", rec.id);
|
|
1596
1647
|
// Same cross-home twin guard as the decision path above.
|
|
1597
|
-
store.putCapture("constraints", rec, !!input.private);
|
|
1648
|
+
const stored = store.putCapture("constraints", rec, !!input.private);
|
|
1649
|
+
const observed = observeReportCapture(root, input.task_id, "constraints", stored, home, !!existing, home === "private" ? store.privateDir ?? undefined : hunchPaths(root).hunch);
|
|
1598
1650
|
store.reindex();
|
|
1599
1651
|
// Propagate the new rule to EVERY assistant's ambient grounding (Cursor/Copilot/
|
|
1600
1652
|
// Windsurf/AGENTS.md/CLAUDE.md), so a correction captured in one assistant is held
|
|
@@ -1606,8 +1658,8 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1606
1658
|
// dirty AGENTS/assistant docs. Manual mode still refreshes in place.
|
|
1607
1659
|
if (home === "public" && !store.autoCommit)
|
|
1608
1660
|
refreshExistingGrounding(root, store); // overlay rules never render into committed grounding
|
|
1609
|
-
const flush = flushCapture(store, hunchPaths(root).hunch, !!input.private, `hunch: capture ${rec.id}`, startupTeamRoute ?? undefined);
|
|
1610
|
-
const flushed = flushNote(flush, home, store.mode) + publicHomeNote(home, store.hasPrivate, rec, hunchPaths(root).hunch);
|
|
1661
|
+
const flush = flushCapture(store, hunchPaths(root).hunch, !!input.private, `hunch: capture ${rec.id}`, startupTeamRoute ?? undefined, observed.observe);
|
|
1662
|
+
const flushed = flushNote(flush, home, store.mode) + publicHomeNote(home, store.hasPrivate, rec, hunchPaths(root).hunch) + observed.note;
|
|
1611
1663
|
const enforce = rec.severity === "blocking"
|
|
1612
1664
|
? "blocks a DIRECT edit to its scope at strict firmness, and fails a PR whose diff touches that scope (CI guard); blast-radius hits and lower firmness stay advisory"
|
|
1613
1665
|
: "flags violating edits and PRs (advisory)";
|
|
@@ -1650,9 +1702,10 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1650
1702
|
resolved_commit: z.string().optional().describe("the commit that fixed it (with triage:'resolved')"),
|
|
1651
1703
|
private: z.boolean().optional().describe("write into the PRIVATE overlay store instead of the committed repo. Errors if no private store is configured."),
|
|
1652
1704
|
}),
|
|
1705
|
+
task_id: TaskIdSchema.optional().describe("Exact task ID for observing this successful save; reporting never changes capture authority."),
|
|
1653
1706
|
cwd: cwdHintField,
|
|
1654
1707
|
},
|
|
1655
|
-
}, async ({ finding }) => {
|
|
1708
|
+
}, async ({ finding, task_id }) => {
|
|
1656
1709
|
try {
|
|
1657
1710
|
if (!finding.title.trim())
|
|
1658
1711
|
return invalid("title is required.");
|
|
@@ -1682,10 +1735,11 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1682
1735
|
resolved_commit: finding.resolved_commit ?? existing?.resolved_commit ?? null,
|
|
1683
1736
|
provenance: { source: "human_confirmed", confidence: 0.95, evidence: finding.evidence ?? existing?.provenance.evidence ?? [], last_verified: now },
|
|
1684
1737
|
};
|
|
1685
|
-
store.putCapture("findings", rec, !!finding.private);
|
|
1738
|
+
const stored = store.putCapture("findings", rec, !!finding.private);
|
|
1739
|
+
const observed = observeReportCapture(root, task_id, "findings", stored, home, !!existing, home === "private" ? store.privateDir ?? undefined : hunchPaths(root).hunch);
|
|
1686
1740
|
store.reindex();
|
|
1687
|
-
const flush = flushCapture(store, hunchPaths(root).hunch, !!finding.private, `hunch: capture ${id}`, startupTeamRoute ?? undefined);
|
|
1688
|
-
const flushed = flushNote(flush, home, store.mode) + publicHomeNote(home, store.hasPrivate, rec, hunchPaths(root).hunch);
|
|
1741
|
+
const flush = flushCapture(store, hunchPaths(root).hunch, !!finding.private, `hunch: capture ${id}`, startupTeamRoute ?? undefined, observed.observe);
|
|
1742
|
+
const flushed = flushNote(flush, home, store.mode) + publicHomeNote(home, store.hasPrivate, rec, hunchPaths(root).hunch) + observed.note;
|
|
1689
1743
|
const where = finding.private
|
|
1690
1744
|
? ` [PRIVATE overlay — not committed to this repo]${flushed}`
|
|
1691
1745
|
: home === "private" ? ` [SHARED store — one source of truth for the whole team]${flushed}` : flushed;
|
|
@@ -1727,7 +1781,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1727
1781
|
});
|
|
1728
1782
|
server.registerTool("nuryel_read", {
|
|
1729
1783
|
title: "nuryel.state/1 read — the system-of-record answer for a subject",
|
|
1730
|
-
description: "Read organizational state under a delivery receipt. Pass the principal (id, kind, grants) and the scope; optionally a subject (an entity id, a decision topic, an external `object_type:object_key`) to get state_of_record — what is current, in force, done, what it depends on and what invalidates it — plus a task phrase for the ranked delivery envelope. Scopes the principal is not granted are named in denied_scopes, never silently dropped.",
|
|
1784
|
+
description: "Read organizational state under a delivery receipt. Pass the principal (id, kind, grants) and the scope; optionally a subject (an entity id, a decision topic, an external `object_type:object_key`) to get state_of_record — what is current, in force, done, what it depends on and what invalidates it — plus a task phrase for the ranked delivery envelope. Scopes the principal is not granted are named in denied_scopes, never silently dropped. To read observations beyond the default 64, use observed_page:{} with one scope and a subject, then pass state_of_record.observed_page.next_cursor as observed_page.cursor until null. A conflict means the observations changed: restart from the first page. Never claim complete coverage while a next cursor remains.",
|
|
1731
1785
|
inputSchema: ReadRequestSchema.omit({ schema: true }).shape,
|
|
1732
1786
|
outputSchema: ReadResponseSchema.shape,
|
|
1733
1787
|
}, async (input) => {
|
|
@@ -1735,7 +1789,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1735
1789
|
const { response, envelope } = readState(store, { schema: STATE_READ_VERSION, ...input });
|
|
1736
1790
|
const sor = response.state_of_record;
|
|
1737
1791
|
const summary = sor
|
|
1738
|
-
? `subject ${sor.subject}: current ${sor.current.length} · in force ${sor.in_force.length} · done ${sor.done.length} · depends on ${sor.depends_on.length} · invalidated by ${sor.invalidated_by.length}`
|
|
1792
|
+
? `subject ${sor.subject}: current ${sor.current.length} · in force ${sor.in_force.length} · done ${sor.done.length} · observed ${sor.observed?.length ?? 0} · depends on ${sor.depends_on.length} · invalidated by ${sor.invalidated_by.length}`
|
|
1739
1793
|
: "no subject — delivery envelope only";
|
|
1740
1794
|
const deniedNote = response.denied_scopes.length ? `\ndenied scopes: ${response.denied_scopes.map((s) => `${s.kind}/${s.id}`).join(", ")}` : "";
|
|
1741
1795
|
// Render the state of record itself, not only its refs: a consumer answers from this text.
|
|
@@ -1774,6 +1828,9 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1774
1828
|
};
|
|
1775
1829
|
const stateText = sor
|
|
1776
1830
|
? [...sor.current.map((r) => line("current", r)), ...sor.in_force.map((r) => line("in force", r)), ...sor.done.map((r) => line("done", r)),
|
|
1831
|
+
...(sor.observed ?? []).map(r => line("observed; verify currentness before relying on it", r)),
|
|
1832
|
+
...(sor.observed_page ? [`- Observation page: ${sor.observed_page.total} total; next_cursor: ${JSON.stringify(sor.observed_page.next_cursor)}`]
|
|
1833
|
+
: sor.observed_truncated ? ['- More observations exist; read this subject with observed_page:{} in one partition, then follow next_cursor.'] : []),
|
|
1777
1834
|
...(sor.invalidated_by.length ? [`- invalidated by: ${sor.invalidated_by.join(", ")}`] : [])].join("\n") || "(nothing on record for this subject)"
|
|
1778
1835
|
: "";
|
|
1779
1836
|
return stateResult(`${response.receipt_id} · ${summary}${deniedNote}${stateText ? `\n\nState of record:\n${stateText}` : ""}\n\n${envelope.text}`, response);
|
|
@@ -1784,7 +1841,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1784
1841
|
});
|
|
1785
1842
|
server.registerTool("nuryel_write", {
|
|
1786
1843
|
title: "nuryel.state/1 write — provenance + idempotency in, durability out",
|
|
1787
|
-
description: "Write one record into a facet (receipts, commitments, derived, entities, relationships, or the legacy decisions/constraints/bugs/findings). The record must carry provenance; the request must carry an idempotency_key — a replay returns the original, a reused key with a different payload is refused. Ids are derived from the record's facts, never chosen. A second live decision on a topic is refused with the incumbent named; pass supersedes to replace it explicitly. organization/team/user partitions never ride a repository: they require an overlay.",
|
|
1844
|
+
description: "Write one record into a facet (receipts, commitments, derived, entities, relationships, or the legacy decisions/constraints/bugs/findings). The record must carry provenance; the request must carry an idempotency_key — a replay returns the original, a reused key with a different payload is refused. Ids are derived from the record's facts, never chosen. A second live decision on a topic is refused with the incumbent named; pass supersedes to replace it explicitly. organization/team/user partitions never ride a repository: they require an overlay. To show an existing captured observation under another subject without copying it, write a relationship type observation_about with from=observation id, to=subject, observation_hash, lifecycle=active, reason and hashed external evidence of the explicit association. Retire the relationship to unlink; reactivation requires expected_version.",
|
|
1788
1845
|
inputSchema: { ...WriteRequestSchema.omit({ schema: true }).shape, cwd: cwdHintField },
|
|
1789
1846
|
outputSchema: WriteResultSchema.shape,
|
|
1790
1847
|
}, async ({ cwd: _cwd, ...input }) => {
|
|
@@ -1800,6 +1857,38 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1800
1857
|
return stateRefusal(e);
|
|
1801
1858
|
}
|
|
1802
1859
|
});
|
|
1860
|
+
server.registerTool("nuryel_capture", {
|
|
1861
|
+
title: "nuryel.state/1 capture — one relevant assertion with exact source excerpts",
|
|
1862
|
+
description: "Save ONE relevant atomic assertion learned during the task. First split mixed source material into independent assertions; check each one, retaining new relevant details inside otherwise known passages. Exclude chatter, speculation, unsupported conclusions and transient tool output. Supply a concrete future-use reason and exact excerpts from the source text. Whole source text is transient and is never stored. Deduplication is per assertion + subject + source excerpt, independent of agent and read time; never skip a whole document because some of it is known. Records are observations, not verified current summaries or execution receipts. Call after substantive learning without waiting for the user to say remember. Read back the returned record before claiming it was saved.",
|
|
1863
|
+
inputSchema: { ...CaptureRequestSchema.omit({ schema: true }).shape, cwd: cwdHintField },
|
|
1864
|
+
outputSchema: WriteResultSchema.shape,
|
|
1865
|
+
}, async ({ cwd: _cwd, ...input }) => {
|
|
1866
|
+
try {
|
|
1867
|
+
const result = await withWriteLock(hunchPaths(root).hunch, () => captureState(store, { schema: STATE_CAPTURE_VERSION, ...input }, {
|
|
1868
|
+
flush: (isPrivate, message) => flushCapture(store, hunchPaths(root).hunch, isPrivate, message, startupTeamRoute ?? undefined),
|
|
1869
|
+
}));
|
|
1870
|
+
return stateResult(`${result.outcome} observation ${result.record_id} (${result.durability}); this does not assert currentness. ${result.record_hash}`, result);
|
|
1871
|
+
}
|
|
1872
|
+
catch (e) {
|
|
1873
|
+
return stateRefusal(e);
|
|
1874
|
+
}
|
|
1875
|
+
});
|
|
1876
|
+
server.registerTool("nuryel_capture_batch", {
|
|
1877
|
+
title: "nuryel.state/1 capture batch — save relevant atomic observations",
|
|
1878
|
+
description: "Preferred capture for multiple facts learned during the task. Split source material into independent relevant assertions, select exact supporting excerpts and give each a concrete future-use reason. Exclude chatter, unsupported inference and transient output. Check every assertion even in a known paragraph: deduplication never discards a whole passage. Send each source once and reference its zero-based index. At most 32 observations and 8 sources; split larger work into batches. One partition lock and index update, no extra model call. Results preserve input indexes; inspect every refusal and stored record. Saved observations have unknown currentness, not verified receipts or current summaries. Use your own initiating agent identity automatically after substantive learning. Optional reviews withdraw specific prior observations: supply record_id, expected_hash, a reason, and exact excerpts from a changed source that observation depends on. Mere hash changes, missing text or uncertain interpretation never suffice; the initiating agent must identify explicit contradiction or withdrawal. Original facts remain in history with the review author and evidence. Review results are separately indexed; inspect every refusal.",
|
|
1879
|
+
inputSchema: { ...CaptureBatchRequestSchema.omit({ schema: true }).shape, cwd: cwdHintField },
|
|
1880
|
+
outputSchema: CaptureBatchResultSchema.shape,
|
|
1881
|
+
}, async ({ cwd: _cwd, ...input }) => {
|
|
1882
|
+
try {
|
|
1883
|
+
const result = await withWriteLock(hunchPaths(root).hunch, () => captureBatchState(store, { schema: STATE_CAPTURE_BATCH_VERSION, ...input }, {
|
|
1884
|
+
flush: (isPrivate, message) => flushCapture(store, hunchPaths(root).hunch, isPrivate, message, startupTeamRoute ?? undefined),
|
|
1885
|
+
}));
|
|
1886
|
+
return stateResult(`Capture batch: ${result.results.filter(r => r.status === "saved").length} saved/replayed, ${result.results.filter(r => r.status === "refused").length} refused.${result.reviews ? ` Reviews: ${result.reviews.filter(r => r.status === "saved").length} withdrawn/replayed, ${result.reviews.filter(r => r.status === "refused").length} refused.` : ''} Inspect each indexed result.`, result);
|
|
1887
|
+
}
|
|
1888
|
+
catch (e) {
|
|
1889
|
+
return stateRefusal(e);
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1803
1892
|
server.registerTool("nuryel_subscribe", {
|
|
1804
1893
|
title: "nuryel.state/1 subscribe — the scope's ordered change stream after a cursor",
|
|
1805
1894
|
description: "Return the change events for a scope with seq > after_seq, strictly ordered. Unfiltered, the events are contiguous (a gap means resynchronize); with facets/subjects filters the response is a subsequence and head_seq is still your next cursor. Each event names the record, its hash, what changed, what it invalidates, and the cause.",
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { TaskIdSchema, ReportClaimSchema, LessonReferenceSchema, finishReportTask, listReportTasks, readTaskReport, readLessonHistory, recordReportClaim, reportPresentationEnabled, startReportTask } from "../core/taskReport.js";
|
|
4
|
+
import { reportSourceSnapshot, runReportConformance } from "../core/taskReportEvidence.js";
|
|
5
|
+
import { renderTaskReport, writeTaskReportHtml } from "../core/taskReportRender.js";
|
|
6
|
+
function applicationReferences(report) {
|
|
7
|
+
return report.deliveries.flatMap(d => d.records.map(r => ({ occurrence_id: d.occurrence_id, record_id: r.record_id, content_hash: r.content_hash, title: r.title }))).slice(-100);
|
|
8
|
+
}
|
|
9
|
+
/** Reuse the MCP server's installation, not a potentially stale global binary.
|
|
10
|
+
* Structured argv is authoritative; the shell hint uses literal quoting. */
|
|
11
|
+
function verificationLauncher() {
|
|
12
|
+
const dev = import.meta.url.endsWith(".ts");
|
|
13
|
+
const entry = fileURLToPath(new URL(`../cli/index.${dev ? "ts" : "js"}`, import.meta.url));
|
|
14
|
+
const argv = [process.execPath, ...(dev ? ["--import", fileURLToPath(import.meta.resolve("tsx"))] : []), entry];
|
|
15
|
+
const quote = (s) => process.platform === "win32" ? `'${s.replace(/'/g, "''")}'` : `'${s.replace(/'/g, "'\\''")}'`;
|
|
16
|
+
return { argv, shell: `${process.platform === "win32" ? "& " : ""}${argv.map(quote).join(" ")}` };
|
|
17
|
+
}
|
|
18
|
+
export function registerTaskReportTools(server, getRoot, getStore) {
|
|
19
|
+
server.registerTool("hunch_task", {
|
|
20
|
+
title: "Start or finish a task's contribution report",
|
|
21
|
+
description: "Start once per user task; pass the returned task_id to hunch_context. Finish before your final response and include the returned concise contribution card, without asking the user. Applications are explicitly agent-reported and must name an exact delivered occurrence and record hash. Completion never implies successful verification. Not for storing decisions or claiming tests passed; use the CLI task verify wrapper for observed command results.",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
action: z.enum(["start", "finish"]), task_id: TaskIdSchema.optional(),
|
|
24
|
+
title: z.string().min(1).max(200).optional(),
|
|
25
|
+
outcome: z.enum(["completed", "interrupted"]).optional(),
|
|
26
|
+
applications: z.array(ReportClaimSchema).max(20).optional(),
|
|
27
|
+
cwd: z.string().optional().describe("Actual repository/worktree directory for this task."),
|
|
28
|
+
},
|
|
29
|
+
}, async ({ action, task_id, title, outcome, applications }) => {
|
|
30
|
+
try {
|
|
31
|
+
const root = getRoot();
|
|
32
|
+
if (action === "start") {
|
|
33
|
+
if (!title || applications?.length || outcome)
|
|
34
|
+
throw new Error("start requires a short task title and no completion evidence");
|
|
35
|
+
const task = startReportTask(root, title, task_id);
|
|
36
|
+
const launcher = verificationLauncher();
|
|
37
|
+
return { content: [{ type: "text", text: `Task ${task.task_id} · ${task.state}. Pass task_id to every hunch_context and decision/correction/finding capture call. Before the final response, finish with hunch_task and include its contribution card. For checks use this exact installation (the global hunch binary may be stale): ${launcher.shell} task verify ${task.task_id} -- <command> [arguments].` }], structuredContent: { task, verification_argv: [...launcher.argv, "task", "verify", task.task_id, "--"] } };
|
|
38
|
+
}
|
|
39
|
+
if (!task_id)
|
|
40
|
+
throw new Error("finish requires the exact task_id");
|
|
41
|
+
for (const claim of applications ?? [])
|
|
42
|
+
recordReportClaim(root, task_id, claim);
|
|
43
|
+
// Hunch's own rule evaluation of the delivered lessons; the agent submits
|
|
44
|
+
// no verdict. Failure is disclosed by the report and never blocks finish.
|
|
45
|
+
if ((outcome ?? "completed") === "completed") {
|
|
46
|
+
try {
|
|
47
|
+
runReportConformance(root, getStore(), task_id);
|
|
48
|
+
}
|
|
49
|
+
catch { /* unknowns disclose it */ }
|
|
50
|
+
}
|
|
51
|
+
finishReportTask(root, task_id, outcome ?? "completed");
|
|
52
|
+
const report = readTaskReport(root, task_id, reportSourceSnapshot(root).hash);
|
|
53
|
+
const show = reportPresentationEnabled(root);
|
|
54
|
+
let file = null;
|
|
55
|
+
try {
|
|
56
|
+
file = writeTaskReportHtml(root, task_id);
|
|
57
|
+
}
|
|
58
|
+
catch { /* retained report remains inspectable through MCP */ }
|
|
59
|
+
const card = file ? renderTaskReport(report).replace(/^Evidence .*$/m, `Evidence [Open local report](<${file}>)`) : renderTaskReport(report);
|
|
60
|
+
return { content: [{ type: "text", text: show ? card : "Task report retained. Automatic presentation is disabled; omit the contribution card from the final response." }], structuredContent: { ...report, presentation_enabled: show, contribution_card: show ? card : null, report_path: file } };
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
const message = `Task report unavailable: ${error.message}`;
|
|
64
|
+
// Some hosts show structuredContent instead of text blocks. Return exact
|
|
65
|
+
// recovery references there as well; never ask an agent to invent a hash.
|
|
66
|
+
let references = [];
|
|
67
|
+
if (task_id) {
|
|
68
|
+
try {
|
|
69
|
+
references = applicationReferences(readTaskReport(getRoot(), task_id));
|
|
70
|
+
}
|
|
71
|
+
catch { }
|
|
72
|
+
}
|
|
73
|
+
return { isError: true, content: [{ type: "text", text: message }], structuredContent: { error: message, application_references: references, recovery: "For a real application, copy an exact reference and add the action you took. Do not replace receipt prefixes or substitute a scope hash. For full evidence call hunch_report with the exact task ID." } };
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
server.registerTool("hunch_report", {
|
|
77
|
+
title: "Inspect the evidence for Hunch's contribution to a task",
|
|
78
|
+
description: "Read task reports: exact delivered memory, agent-reported applications, observed command results and explicit unknowns. Supply lesson for exact revision history across tasks. With neither task_id nor lesson, lists recent tasks without guessing which is yours. html writes a local private evidence view. Not a causal impact score, public export, or authority to execute verification commands.",
|
|
79
|
+
inputSchema: { task_id: TaskIdSchema.optional(), lesson: LessonReferenceSchema.optional().describe("Exact kind and record_id, optionally content_hash, to inspect retained appearances across tasks. Partial indexing requires refreshing before pagination."), before: z.number().int().positive().optional(), html: z.boolean().optional(), cwd: z.string().optional() },
|
|
80
|
+
}, async ({ task_id, lesson, before, html }) => {
|
|
81
|
+
try {
|
|
82
|
+
const root = getRoot();
|
|
83
|
+
if (lesson) {
|
|
84
|
+
if (task_id || html)
|
|
85
|
+
throw new Error("choose lesson history or a task report, not both");
|
|
86
|
+
const history = readLessonHistory(root, lesson, { before });
|
|
87
|
+
return { content: [{ type: "text", text: JSON.stringify(history) }], structuredContent: history };
|
|
88
|
+
}
|
|
89
|
+
if (before !== undefined)
|
|
90
|
+
throw new Error("before requires a lesson reference");
|
|
91
|
+
if (!task_id) {
|
|
92
|
+
if (html)
|
|
93
|
+
throw new Error("html requires an exact task_id");
|
|
94
|
+
const tasks = listReportTasks(root);
|
|
95
|
+
return { content: [{ type: "text", text: tasks.length ? tasks.map(t => `${t.task_id} · ${t.state} · ${t.title}`).join("\n") : "No task reports yet. Start with hunch_task." }], structuredContent: { tasks } };
|
|
96
|
+
}
|
|
97
|
+
const report = readTaskReport(root, task_id, reportSourceSnapshot(root).hash);
|
|
98
|
+
const file = html ? writeTaskReportHtml(root, task_id) : null;
|
|
99
|
+
return { content: [{ type: "text", text: `${renderTaskReport(report)}${file ? `\nLocal evidence view: ${file}` : ""}` }], structuredContent: { ...report, application_references: applicationReferences(report), contribution_card: renderTaskReport(report), report_path: file } };
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return { isError: true, content: [{ type: "text", text: `Invalid: ${error.message}` }] };
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=taskReportTools.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hunch serve` — the HTTP binding of nuryel.state/1, and the served product's partition host.
|
|
3
|
+
*
|
|
4
|
+
* Folded in from Hunch Memory: bind 127.0.0.1 (the caller's job; never expose a port), a
|
|
5
|
+
* bearer token that resolves the PRINCIPAL (the body never names one — grants come from the
|
|
6
|
+
* config, never from the caller), problem+json errors, a body limit, and a cross-process
|
|
7
|
+
* write lock per partition so a stdio MCP process on the same store cannot race a write.
|
|
8
|
+
*
|
|
9
|
+
* Every rule lives in src/store/stateBinding.ts; this file only maps HTTP to it:
|
|
10
|
+
* GET /nuryel/v1/capabilities → capabilities of the partition named by ?scope=kind:id (default: first granted)
|
|
11
|
+
* POST /nuryel/v1/read → readState; with `scopes` a UNION read: readState per granted
|
|
12
|
+
* served partition, merged by mergeReadResponses (primary's envelope)
|
|
13
|
+
* POST /nuryel/v1/write → writeState (under the partition's write lock)
|
|
14
|
+
* POST /nuryel/v1/subscribe → subscribeState
|
|
15
|
+
* POST /nuryel/v1/records → recordsState (by id, grants first)
|
|
16
|
+
* Request bodies are the contract's request schemas minus `schema` and `principal`.
|
|
17
|
+
*/
|
|
18
|
+
import { type Server } from "node:http";
|
|
19
|
+
import { HunchStore } from "../store/hunchStore.js";
|
|
20
|
+
import { type ServeConfig } from "./config.js";
|
|
21
|
+
export declare const BODY_LIMIT_BYTES: number;
|
|
22
|
+
export declare const PROBLEM_TYPE = "https://www.hunchmemory.com/problems/nuryel.state/1/";
|
|
23
|
+
export declare class HttpProblem extends Error {
|
|
24
|
+
readonly status: number;
|
|
25
|
+
readonly code: string;
|
|
26
|
+
readonly extra: Record<string, unknown>;
|
|
27
|
+
constructor(status: number, code: string, message: string, extra?: Record<string, unknown>);
|
|
28
|
+
}
|
|
29
|
+
export interface ServeOptions {
|
|
30
|
+
version?: string;
|
|
31
|
+
/** Injectable for tests: how a partition's store is opened. */
|
|
32
|
+
openStore?: (root: string) => HunchStore;
|
|
33
|
+
}
|
|
34
|
+
export declare function createServeApp(config: ServeConfig, opts?: ServeOptions): Server & {
|
|
35
|
+
closeStores: () => void;
|
|
36
|
+
};
|
package/dist/serve/app.js
CHANGED
|
@@ -24,6 +24,8 @@ import { STATE_READ_VERSION, STATE_RECORDS_VERSION, STATE_SUBSCRIBE_VERSION, STA
|
|
|
24
24
|
import { partitionFor, resolvePrincipal } from "./config.js";
|
|
25
25
|
import { WriteLockTimeout, withWriteLock } from "./writelock.js";
|
|
26
26
|
import { HUNCH_VERSION } from "../core/version.js";
|
|
27
|
+
import { captureState, captureBatchState } from "../store/stateCapture.js";
|
|
28
|
+
import { STATE_CAPTURE_VERSION, STATE_CAPTURE_BATCH_VERSION } from "../core/stateContract.js";
|
|
27
29
|
export const BODY_LIMIT_BYTES = 1024 * 1024;
|
|
28
30
|
export const PROBLEM_TYPE = "https://www.hunchmemory.com/problems/nuryel.state/1/";
|
|
29
31
|
export class HttpProblem extends Error {
|
|
@@ -148,6 +150,8 @@ export function createServeApp(config, opts = {}) {
|
|
|
148
150
|
delete body.schema;
|
|
149
151
|
if (url.pathname === "/nuryel/v1/read") {
|
|
150
152
|
const scope = requireScope(principal, body);
|
|
153
|
+
if (body.observed_page !== undefined && body.scopes !== undefined)
|
|
154
|
+
throw problem(400, 'malformed', 'observation pages require a single partition without scopes');
|
|
151
155
|
const { store } = storeFor(scope);
|
|
152
156
|
if (body.scopes === undefined) {
|
|
153
157
|
const { response, envelope } = readState(store, { schema: STATE_READ_VERSION, principal, ...body });
|
|
@@ -178,6 +182,22 @@ export function createServeApp(config, opts = {}) {
|
|
|
178
182
|
}));
|
|
179
183
|
return send(res, result.outcome === "created" ? 201 : 200, result);
|
|
180
184
|
}
|
|
185
|
+
if (url.pathname === "/nuryel/v1/capture") {
|
|
186
|
+
const scope = requireScope(principal, body);
|
|
187
|
+
const { store } = storeFor(scope);
|
|
188
|
+
const result = await withWriteLock(hunchPaths(store.publicRoot).hunch, () => captureState(store, { schema: STATE_CAPTURE_VERSION, principal, ...body }, {
|
|
189
|
+
flush: (isPrivate, message) => flushCapture(store, hunchPaths(store.publicRoot).hunch, isPrivate, message),
|
|
190
|
+
}));
|
|
191
|
+
return send(res, result.outcome === "created" ? 201 : 200, result);
|
|
192
|
+
}
|
|
193
|
+
if (url.pathname === "/nuryel/v1/capture-batch") {
|
|
194
|
+
const scope = requireScope(principal, body);
|
|
195
|
+
const { store, root } = storeFor(scope);
|
|
196
|
+
const result = await withWriteLock(hunchPaths(root).hunch, () => captureBatchState(store, { schema: STATE_CAPTURE_BATCH_VERSION, principal, ...body }, {
|
|
197
|
+
flush: (isPrivate, message) => flushCapture(store, hunchPaths(root).hunch, isPrivate, message),
|
|
198
|
+
}));
|
|
199
|
+
return send(res, 200, result);
|
|
200
|
+
}
|
|
181
201
|
if (url.pathname === "/nuryel/v1/subscribe") {
|
|
182
202
|
const scope = requireScope(principal, body);
|
|
183
203
|
const { store } = storeFor(scope);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type Principal, type Scope } from "../core/stateContract.js";
|
|
3
|
+
export declare const SERVE_CONFIG_VERSION: "nuryel.serve-config/1";
|
|
4
|
+
export declare const PartitionConfigSchema: z.ZodObject<{
|
|
5
|
+
scope: z.ZodObject<{
|
|
6
|
+
kind: z.ZodEnum<{
|
|
7
|
+
repository: "repository";
|
|
8
|
+
organization: "organization";
|
|
9
|
+
team: "team";
|
|
10
|
+
user: "user";
|
|
11
|
+
}>;
|
|
12
|
+
id: z.ZodString;
|
|
13
|
+
}, z.core.$strict>;
|
|
14
|
+
root: z.ZodString;
|
|
15
|
+
}, z.core.$strict>;
|
|
16
|
+
export type PartitionConfig = z.infer<typeof PartitionConfigSchema>;
|
|
17
|
+
export declare const PrincipalConfigSchema: z.ZodObject<{
|
|
18
|
+
id: z.ZodString;
|
|
19
|
+
kind: z.ZodEnum<{
|
|
20
|
+
service: "service";
|
|
21
|
+
human: "human";
|
|
22
|
+
agent: "agent";
|
|
23
|
+
}>;
|
|
24
|
+
display: z.ZodOptional<z.ZodString>;
|
|
25
|
+
token_sha256: z.ZodString;
|
|
26
|
+
grants: z.ZodArray<z.ZodObject<{
|
|
27
|
+
kind: z.ZodEnum<{
|
|
28
|
+
repository: "repository";
|
|
29
|
+
organization: "organization";
|
|
30
|
+
team: "team";
|
|
31
|
+
user: "user";
|
|
32
|
+
}>;
|
|
33
|
+
id: z.ZodString;
|
|
34
|
+
}, z.core.$strict>>;
|
|
35
|
+
}, z.core.$strict>;
|
|
36
|
+
export type PrincipalConfig = z.infer<typeof PrincipalConfigSchema>;
|
|
37
|
+
export declare const ServeConfigSchema: z.ZodObject<{
|
|
38
|
+
schema: z.ZodLiteral<"nuryel.serve-config/1">;
|
|
39
|
+
port: z.ZodDefault<z.ZodNumber>;
|
|
40
|
+
partitions: z.ZodArray<z.ZodObject<{
|
|
41
|
+
scope: z.ZodObject<{
|
|
42
|
+
kind: z.ZodEnum<{
|
|
43
|
+
repository: "repository";
|
|
44
|
+
organization: "organization";
|
|
45
|
+
team: "team";
|
|
46
|
+
user: "user";
|
|
47
|
+
}>;
|
|
48
|
+
id: z.ZodString;
|
|
49
|
+
}, z.core.$strict>;
|
|
50
|
+
root: z.ZodString;
|
|
51
|
+
}, z.core.$strict>>;
|
|
52
|
+
principals: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
53
|
+
id: z.ZodString;
|
|
54
|
+
kind: z.ZodEnum<{
|
|
55
|
+
service: "service";
|
|
56
|
+
human: "human";
|
|
57
|
+
agent: "agent";
|
|
58
|
+
}>;
|
|
59
|
+
display: z.ZodOptional<z.ZodString>;
|
|
60
|
+
token_sha256: z.ZodString;
|
|
61
|
+
grants: z.ZodArray<z.ZodObject<{
|
|
62
|
+
kind: z.ZodEnum<{
|
|
63
|
+
repository: "repository";
|
|
64
|
+
organization: "organization";
|
|
65
|
+
team: "team";
|
|
66
|
+
user: "user";
|
|
67
|
+
}>;
|
|
68
|
+
id: z.ZodString;
|
|
69
|
+
}, z.core.$strict>>;
|
|
70
|
+
}, z.core.$strict>>>;
|
|
71
|
+
}, z.core.$strict>;
|
|
72
|
+
export type ServeConfig = z.infer<typeof ServeConfigSchema>;
|
|
73
|
+
export declare const PARTITION_GITIGNORE: string;
|
|
74
|
+
export declare function hashToken(token: string): string;
|
|
75
|
+
export declare function mintToken(): string;
|
|
76
|
+
export declare function readServeConfig(file: string): ServeConfig & {
|
|
77
|
+
file: string;
|
|
78
|
+
};
|
|
79
|
+
export declare function writeServeConfig(file: string, config: ServeConfig): void;
|
|
80
|
+
/** Constant-time token → principal. Undefined for a missing or unknown token. */
|
|
81
|
+
export declare function resolvePrincipal(config: ServeConfig, token: string | undefined): Principal | undefined;
|
|
82
|
+
export declare function partitionFor(config: ServeConfig, scope: Scope): PartitionConfig | undefined;
|
|
83
|
+
/** `serve init`: ensure a partition directory declares its scope, and add a principal
|
|
84
|
+
* with a freshly minted token. Idempotent for the partition; a principal id that
|
|
85
|
+
* already exists gets a NEW token (rotation), the old one stops working. */
|
|
86
|
+
export declare function initServeConfig(opts: {
|
|
87
|
+
file: string;
|
|
88
|
+
scope: Scope;
|
|
89
|
+
root: string;
|
|
90
|
+
principal?: {
|
|
91
|
+
id: string;
|
|
92
|
+
kind: "human" | "agent" | "service";
|
|
93
|
+
grants?: Scope[];
|
|
94
|
+
};
|
|
95
|
+
port?: number;
|
|
96
|
+
}): {
|
|
97
|
+
config: ServeConfig;
|
|
98
|
+
token: string | null;
|
|
99
|
+
partition: PartitionConfig;
|
|
100
|
+
};
|