specpi 0.30.0 → 0.30.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/CHANGELOG.md +14 -0
- package/NPM_RELEASE.md +2 -2
- package/README.md +4 -5
- package/SECURITY_MODEL.md +10 -6
- package/extensions/jev-advisor/broker.mjs +87 -8
- package/extensions/jev-advisor/client.mjs +33 -10
- package/extensions/jev-advisor/consent.mjs +19 -7
- package/extensions/jev-advisor/gate.mjs +20 -5
- package/extensions/jev-advisor/index.ts +307 -189
- package/extensions/jev-advisor/ledger.mjs +16 -2
- package/extensions/jev-advisor/questions/gap.mjs +49 -86
- package/extensions/jev-advisor/questions/retention.mjs +5 -8
- package/extensions/jev-advisor/questions/sources.mjs +27 -56
- package/extensions/jev-advisor/sanitize.mjs +0 -0
- package/extensions/tool-wishlist/core.mjs +86 -0
- package/extensions/tool-wishlist/index.ts +81 -8
- package/extensions/workflow-controls/index.ts +23 -0
- package/package.json +2 -2
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
setCollectionMode,
|
|
32
32
|
wishlistSourceRootIdentity,
|
|
33
33
|
wishlistSourceRootSalt,
|
|
34
|
+
wishlistTriageCandidates,
|
|
34
35
|
} from "./core.mjs";
|
|
35
36
|
import { VALIDATOR_CATALOG } from "./validators.mjs";
|
|
36
37
|
import { isValidValidatorName, validateCapabilityRegistry } from "./registry.mjs";
|
|
@@ -608,9 +609,19 @@ export default function toolWishlist(pi: ExtensionAPI) {
|
|
|
608
609
|
}
|
|
609
610
|
};
|
|
610
611
|
|
|
612
|
+
// Earlier hooks may await objective lookup before our input/start hook runs. Invalidate
|
|
613
|
+
// pending reports synchronously when that lookup's owner observes the task change.
|
|
614
|
+
pi.events?.on?.("specpi:task-changing", () => {
|
|
615
|
+
activeRunId = randomUUID();
|
|
616
|
+
});
|
|
611
617
|
pi.on("before_agent_start", async () => {
|
|
612
618
|
activeRunId = randomUUID();
|
|
613
619
|
});
|
|
620
|
+
pi.on("input", (event: any) => {
|
|
621
|
+
if (event.streamingBehavior === "steer" && event.source !== "extension") {
|
|
622
|
+
activeRunId = randomUUID();
|
|
623
|
+
}
|
|
624
|
+
});
|
|
614
625
|
|
|
615
626
|
const displayMarkdown = async (markdown: string, reportPath: string, ctx: any) => {
|
|
616
627
|
const display = truncateReportDisplay(markdown);
|
|
@@ -642,7 +653,7 @@ export default function toolWishlist(pi: ExtensionAPI) {
|
|
|
642
653
|
name: "report_capability_gap",
|
|
643
654
|
label: "Report Capability Gap",
|
|
644
655
|
description:
|
|
645
|
-
"Privately record a material, reusable capability gap in SpecPi's local wishlist. Report only after reasonable existing tools or workarounds proved insufficient. Do not use for transient failures, command mistakes, credentials or permissions the user must supply, ordinary project-specific work, or speculative nice-to-haves. Never include secrets, source code, full commands, file paths, URLs with private data, or user prompt text. Report a gap at most once per user task. Collection requires an explicit local on/off decision and
|
|
656
|
+
"Privately record a material, reusable capability gap in SpecPi's local wishlist. Report only after reasonable existing tools or workarounds proved insufficient. Do not use for transient failures, command mistakes, credentials or permissions the user must supply, ordinary project-specific work, or speculative nice-to-haves. Never include secrets, source code, full commands, file paths, URLs with private data, or user prompt text. Report a gap at most once per user task. Collection requires an explicit local on/off decision. Only a separately enabled and consented Jev advisor may transmit redacted report samples.",
|
|
646
657
|
promptSnippet: "Record recurring, generalizable capability friction without interrupting the user task",
|
|
647
658
|
promptGuidelines: [
|
|
648
659
|
"Use report_capability_gap only for a material and generalizable missing capability after reasonable existing tools or workarounds have proved insufficient.",
|
|
@@ -685,6 +696,27 @@ export default function toolWishlist(pi: ExtensionAPI) {
|
|
|
685
696
|
{ additionalProperties: false },
|
|
686
697
|
),
|
|
687
698
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
699
|
+
// Bind before consent or root discovery can yield to another task or branch.
|
|
700
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
701
|
+
const generation = improvementLifecycleGeneration;
|
|
702
|
+
const reportRun = activeRunId;
|
|
703
|
+
const contractIdentity = () =>
|
|
704
|
+
JSON.stringify(
|
|
705
|
+
branchEntries(ctx).findLast((entry: any) => entry.customType === TASK_CONTRACT_ENTRY)?.data ?? null,
|
|
706
|
+
);
|
|
707
|
+
const originalContract = contractIdentity();
|
|
708
|
+
const isCurrent = () =>
|
|
709
|
+
!signal?.aborted &&
|
|
710
|
+
generation === improvementLifecycleGeneration &&
|
|
711
|
+
sessionId === ctx.sessionManager.getSessionId() &&
|
|
712
|
+
reportRun === activeRunId &&
|
|
713
|
+
originalContract === contractIdentity();
|
|
714
|
+
const assertCurrent = () => {
|
|
715
|
+
if (!isCurrent()) {
|
|
716
|
+
throw new Error("The task or session changed before the report could be recorded");
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
|
|
688
720
|
let mode = readCollectionMode(stateDir);
|
|
689
721
|
if (mode === "undecided") {
|
|
690
722
|
if (!ctx.hasUI) {
|
|
@@ -701,8 +733,9 @@ export default function toolWishlist(pi: ExtensionAPI) {
|
|
|
701
733
|
|
|
702
734
|
const enabled = await ctx.ui.confirm(
|
|
703
735
|
"Enable local capability-gap collection?",
|
|
704
|
-
"SpecPi stores sanitized summaries and salted task, session, and project hashes locally.
|
|
736
|
+
"SpecPi stores sanitized summaries and salted task, session, and project hashes locally. Collection itself never uploads them; the optional Jev advisor requires separate transmission consent. Change collection with /wishlist on or /wishlist off.",
|
|
705
737
|
);
|
|
738
|
+
assertCurrent();
|
|
706
739
|
mode = enabled ? "on" : "off";
|
|
707
740
|
await setCollectionMode({ stateDir, mode, signal });
|
|
708
741
|
}
|
|
@@ -720,15 +753,52 @@ export default function toolWishlist(pi: ExtensionAPI) {
|
|
|
720
753
|
}
|
|
721
754
|
|
|
722
755
|
const contractRoot = await resolveTaskContractRoot(pi, ctx.cwd, signal);
|
|
756
|
+
assertCurrent();
|
|
723
757
|
const contract = branchTaskContract(ctx, contractRoot, { tolerateMalformed: true });
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
758
|
+
const runId = contract?.id ?? reportRun;
|
|
759
|
+
const record = (assessment?: any, authority: any = {}) => {
|
|
760
|
+
assertCurrent();
|
|
761
|
+
|
|
762
|
+
return recordCapabilityGap({
|
|
763
|
+
stateDir,
|
|
764
|
+
sessionId,
|
|
765
|
+
runId,
|
|
766
|
+
cwd: ctx.cwd,
|
|
767
|
+
gap: params,
|
|
768
|
+
assessment,
|
|
769
|
+
signal,
|
|
770
|
+
isCurrent,
|
|
771
|
+
assessmentIsCurrent: authority.isCurrent,
|
|
772
|
+
onRecorded: authority.onRecorded,
|
|
773
|
+
});
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
// A trusted extension callback, not a field in model-authored arguments. No advisor
|
|
777
|
+
// is required, and the original report is never overwritten by its second opinion.
|
|
778
|
+
const pending: Promise<any>[] = [];
|
|
779
|
+
pi.events?.emit("specpi:gap-triage", {
|
|
729
780
|
gap: params,
|
|
781
|
+
ctx,
|
|
730
782
|
signal,
|
|
783
|
+
record,
|
|
784
|
+
isCurrent: () => isCurrent() && readCollectionMode(stateDir) === "on",
|
|
785
|
+
existing: () => wishlistTriageCandidates(stateDir),
|
|
786
|
+
reply: (value: Promise<any>) => pending.push(value),
|
|
731
787
|
});
|
|
788
|
+
// Classifier failures leave normal collection available; `record` still checks its
|
|
789
|
+
// task binding and the collection switch before any write.
|
|
790
|
+
const triage = pending.length === 1 ? await pending[0].catch(() => undefined) : undefined;
|
|
791
|
+
if (triage?.blocked) {
|
|
792
|
+
throw new Error(
|
|
793
|
+
"This report appears to contain sensitive or machine-specific details. Remove the specifics and report it again.",
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (triage?.cancelled) {
|
|
798
|
+
throw new Error("The session changed before the report could be recorded");
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const result = triage?.stored ?? (await record());
|
|
732
802
|
const disposition = result.duplicate
|
|
733
803
|
? "Already recorded for this task"
|
|
734
804
|
: result.regression
|
|
@@ -1380,7 +1450,10 @@ export default function toolWishlist(pi: ExtensionAPI) {
|
|
|
1380
1450
|
|
|
1381
1451
|
if (action === "on" || action === "off") {
|
|
1382
1452
|
await setCollectionMode({ stateDir, mode: action });
|
|
1383
|
-
ctx.ui.notify(
|
|
1453
|
+
ctx.ui.notify(
|
|
1454
|
+
`Local wishlist collection is ${action}. Jev transmission requires its own switch and consent.`,
|
|
1455
|
+
"info",
|
|
1456
|
+
);
|
|
1384
1457
|
|
|
1385
1458
|
return;
|
|
1386
1459
|
}
|
|
@@ -366,6 +366,29 @@ export default function workflowControls(pi: ExtensionAPI) {
|
|
|
366
366
|
sessionRestore = restoreSession(ctx).catch(() => {});
|
|
367
367
|
};
|
|
368
368
|
|
|
369
|
+
// Serve the active contract structurally, independent of before_agent_start load order.
|
|
370
|
+
// The advisor receives only the objective, never a branch transcript or rendered headings.
|
|
371
|
+
pi.events.on("specpi:task-objective", (request: any) => {
|
|
372
|
+
request.reply(
|
|
373
|
+
(async () => {
|
|
374
|
+
const ctx = request.ctx as ExtensionContext;
|
|
375
|
+
const origin = captureSession(ctx);
|
|
376
|
+
await sessionRestore;
|
|
377
|
+
if (!sessionIsCurrent(origin, ctx)) {
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
const contract = readCurrentTaskContract(ctx, scope.root);
|
|
383
|
+
|
|
384
|
+
return contract ? { objective: contract.objective, digest: contract.digest } : undefined;
|
|
385
|
+
} catch {
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
})(),
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
|
|
369
392
|
// Web access ships hidden. A missing or unreadable preference means off, and the
|
|
370
393
|
// gate only ever touches its own four tool names.
|
|
371
394
|
let webAccessEnabled = loadStartupActivation();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specpi",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.1",
|
|
4
4
|
"description": "Scope control and a human-selected harness improvement loop for Pi",
|
|
5
5
|
"author": "Tanner Middleton",
|
|
6
6
|
"repository": {
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"check:package": "node scripts/check-package.mjs",
|
|
84
84
|
"check:pi-package": "node scripts/check-pi-package.mjs",
|
|
85
85
|
"check:base": "node scripts/check-base-packages.mjs",
|
|
86
|
-
"check:site": "node scripts/check-site.mjs",
|
|
86
|
+
"check:site": "node scripts/jev-diagrams.mjs --check && node scripts/check-site.mjs",
|
|
87
87
|
"check:browser-qa": "npm --prefix packages/browser-qa run check",
|
|
88
88
|
"check:delegation": "npm --prefix packages/delegation run check",
|
|
89
89
|
"check:experiments": "npm --prefix packages/experiments run check",
|