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.
@@ -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 never uploads data.",
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. It never uploads them. You can change this later with /wishlist on or /wishlist off.",
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 result = await recordCapabilityGap({
725
- stateDir,
726
- sessionId: ctx.sessionManager.getSessionId(),
727
- runId: contract?.id ?? activeRunId,
728
- cwd: ctx.cwd,
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(`Local wishlist collection is ${action}. No data is uploaded.`, "info");
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.0",
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",