@devspecai/pi-plugin 0.0.0-stage → 0.10.3
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 +899 -0
- package/LICENSE +21 -0
- package/README.md +103 -2
- package/extensions/.gitkeep +0 -0
- package/extensions/core/activation-recovery.ts +135 -0
- package/extensions/core/active-deliveries.ts +171 -0
- package/extensions/core/active-session-plans.ts +374 -0
- package/extensions/core/adapter-overlap.ts +247 -0
- package/extensions/core/assistant-speech.ts +44 -0
- package/extensions/core/attach-context.ts +25 -0
- package/extensions/core/attachment-limits.ts +20 -0
- package/extensions/core/attachments.ts +487 -0
- package/extensions/core/auth-store.ts +91 -0
- package/extensions/core/automation-run-retry.ts +31 -0
- package/extensions/core/background-journal.ts +85 -0
- package/extensions/core/background-observer.ts +279 -0
- package/extensions/core/bond-recovery.ts +65 -0
- package/extensions/core/bond-state.ts +80 -0
- package/extensions/core/clear-conversation.ts +53 -0
- package/extensions/core/client-versions.ts +56 -0
- package/extensions/core/client.ts +704 -0
- package/extensions/core/command-offer.ts +88 -0
- package/extensions/core/command-runner.ts +62 -0
- package/extensions/core/command-turn.ts +88 -0
- package/extensions/core/commit-ledger.ts +195 -0
- package/extensions/core/commit-stamp.ts +98 -0
- package/extensions/core/config.ts +63 -0
- package/extensions/core/connection-context.ts +28 -0
- package/extensions/core/context-budget.ts +175 -0
- package/extensions/core/control-result-outbox.ts +80 -0
- package/extensions/core/control.ts +156 -0
- package/extensions/core/ctx-guard.ts +33 -0
- package/extensions/core/debug.ts +136 -0
- package/extensions/core/delivery-queue.ts +92 -0
- package/extensions/core/dispatch.ts +108 -0
- package/extensions/core/duck.ts +128 -0
- package/extensions/core/git-observer.ts +382 -0
- package/extensions/core/github-identity.ts +26 -0
- package/extensions/core/implementation-contract.ts +773 -0
- package/extensions/core/instructions.ts +40 -0
- package/extensions/core/interaction-events.ts +834 -0
- package/extensions/core/json-schema.ts +16 -0
- package/extensions/core/local-activity.ts +34 -0
- package/extensions/core/mcp-config-guard.ts +117 -0
- package/extensions/core/model-catalog.ts +76 -0
- package/extensions/core/model-surface.ts +125 -0
- package/extensions/core/notice-reply.ts +215 -0
- package/extensions/core/owner-inject.ts +148 -0
- package/extensions/core/post-session-receipt.ts +70 -0
- package/extensions/core/preempt.ts +100 -0
- package/extensions/core/project-choice.ts +145 -0
- package/extensions/core/project-scope.ts +335 -0
- package/extensions/core/project-selection-ui.ts +202 -0
- package/extensions/core/provenance-refs.ts +158 -0
- package/extensions/core/push-channel.ts +281 -0
- package/extensions/core/question-receipt-lifecycle.ts +158 -0
- package/extensions/core/recall.ts +121 -0
- package/extensions/core/remote-payload.ts +1329 -0
- package/extensions/core/remote-session-arg.ts +24 -0
- package/extensions/core/repository-context.ts +35 -0
- package/extensions/core/request-chain.ts +1244 -0
- package/extensions/core/result-projection.ts +939 -0
- package/extensions/core/result.ts +100 -0
- package/extensions/core/review-context.ts +70 -0
- package/extensions/core/review-watch.ts +94 -0
- package/extensions/core/room-access.ts +12 -0
- package/extensions/core/room-context.ts +178 -0
- package/extensions/core/room-reader.ts +308 -0
- package/extensions/core/room-store.ts +381 -0
- package/extensions/core/room-sync.ts +79 -0
- package/extensions/core/room-tools.ts +86 -0
- package/extensions/core/room-unread.ts +173 -0
- package/extensions/core/session-events.ts +62 -0
- package/extensions/core/session-polls.ts +292 -0
- package/extensions/core/settings.ts +39 -0
- package/extensions/core/status-line.ts +432 -0
- package/extensions/core/still-to-discuss.ts +124 -0
- package/extensions/core/subagent-fleet.ts +238 -0
- package/extensions/core/system-notice.ts +227 -0
- package/extensions/core/telemetry.ts +177 -0
- package/extensions/core/tool-selection.ts +72 -0
- package/extensions/core/trail-events.ts +247 -0
- package/extensions/core/trail-projection.ts +373 -0
- package/extensions/core/trail.ts +465 -0
- package/extensions/core/turn-lifecycle.ts +472 -0
- package/extensions/core/version-mismatch.ts +165 -0
- package/extensions/core/work-prompt.ts +44 -0
- package/extensions/core/work-state.ts +179 -0
- package/extensions/devspec-adapter-guard.ts +106 -0
- package/extensions/devspec-auth.ts +191 -0
- package/extensions/devspec-brand.ts +140 -0
- package/extensions/devspec-commit-ledger.ts +477 -0
- package/extensions/devspec-context.ts +146 -0
- package/extensions/devspec-memory.ts +136 -0
- package/extensions/devspec-provenance-assistance.ts +233 -0
- package/extensions/devspec-remote.ts +5152 -0
- package/extensions/devspec-tools.ts +552 -0
- package/extensions/devspec-work.ts +244 -0
- package/extensions/generated/README.md +18 -0
- package/extensions/generated/mcp-catalog.ts +15811 -0
- package/package.json +71 -4
- package/prompts/.gitkeep +0 -0
- package/runtime/commit-stamp.mjs +279 -0
- package/runtime/git-observer-shim.mjs +390 -0
- package/skills/.gitkeep +0 -0
- package/themes/.gitkeep +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DevSpec Pi — proactive memory & signals (Phase 5).
|
|
3
|
+
*
|
|
4
|
+
* Two mechanical behaviors, zero model compliance:
|
|
5
|
+
*
|
|
6
|
+
* 1. AUTO-RECALL — every substantive prompt is searched against project
|
|
7
|
+
* memory before the turn starts; new hits are injected.
|
|
8
|
+
* 2. REVIEW NUDGE — while review questions seen on records are open (possibly
|
|
9
|
+
* out of date, waiting for someone's OK), a nudge injects (only when the
|
|
10
|
+
* open set CHANGES, so it doesn't spam).
|
|
11
|
+
*
|
|
12
|
+
* CACHE DISCIPLINE (learned from production stats): both inject as a TAIL
|
|
13
|
+
* message via before_agent_start's message channel — NEVER as system-prompt
|
|
14
|
+
* additions. Provider prompt caches are prefix-based; mutating the system
|
|
15
|
+
* prompt per turn invalidates the entire conversation's cache (observed:
|
|
16
|
+
* CH 0.1%). A message injected just before the new user turn preserves the
|
|
17
|
+
* whole prefix. The static workflow block (devspec-context) stays in the
|
|
18
|
+
* system prompt because it is byte-identical every turn.
|
|
19
|
+
*
|
|
20
|
+
* Cost discipline: one search_memories call per substantive turn, none for
|
|
21
|
+
* short/command prompts; no calls at all when logged out; a recall failure
|
|
22
|
+
* never blocks a turn.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import { resolveToken } from "./core/auth-store.ts";
|
|
27
|
+
import { callDevspec, MODEL_TIMEOUT_MS } from "./core/client.ts";
|
|
28
|
+
import { unwrapMcpResult } from "./core/result.ts";
|
|
29
|
+
import { debugRemote } from "./core/debug.ts";
|
|
30
|
+
import { noteInjectedContext } from "./core/context-budget.ts";
|
|
31
|
+
import {
|
|
32
|
+
cardKey,
|
|
33
|
+
extractCards,
|
|
34
|
+
formatRecallBlock,
|
|
35
|
+
isAgreed,
|
|
36
|
+
recallQuery,
|
|
37
|
+
shouldRecall,
|
|
38
|
+
unseenCards,
|
|
39
|
+
} from "./core/recall.ts";
|
|
40
|
+
import { formatReviewNudge, listFlags, observeReviewResult } from "./core/review-watch.ts";
|
|
41
|
+
import { reviewScope, snapshotHash } from "./core/review-context.ts";
|
|
42
|
+
import { getBond } from "./core/bond-state.ts";
|
|
43
|
+
|
|
44
|
+
export default function (pi: ExtensionAPI, deps = { resolveToken, callDevspec, getBond }) {
|
|
45
|
+
/** Immutable snapshots already injected, isolated by account/project/session. */
|
|
46
|
+
const injectedByScope = new Map<string, Set<string>>();
|
|
47
|
+
/** Last injected review-set signature — nudge re-injects only on change. */
|
|
48
|
+
let lastNudgeSig = "";
|
|
49
|
+
|
|
50
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
51
|
+
const resolved = await deps.resolveToken();
|
|
52
|
+
if (!resolved) return undefined;
|
|
53
|
+
|
|
54
|
+
const bond = deps.getBond();
|
|
55
|
+
const scope = reviewScope(ctx.sessionManager.getSessionId(), bond?.projectId ?? ctx.cwd, resolved.token);
|
|
56
|
+
const injected = injectedByScope.get(scope) ?? new Set<string>();
|
|
57
|
+
injectedByScope.set(scope, injected);
|
|
58
|
+
const parts: string[] = [];
|
|
59
|
+
|
|
60
|
+
// 1. Auto-recall.
|
|
61
|
+
if (shouldRecall(event.prompt)) {
|
|
62
|
+
const query = recallQuery(event.prompt);
|
|
63
|
+
try {
|
|
64
|
+
const raw = await deps.callDevspec(
|
|
65
|
+
resolved.token,
|
|
66
|
+
"search_memories",
|
|
67
|
+
{ query, limit: 8 },
|
|
68
|
+
{ cwd: ctx.cwd, lane: "work", timeoutMs: MODEL_TIMEOUT_MS },
|
|
69
|
+
);
|
|
70
|
+
const { details, isError } = unwrapMcpResult(raw);
|
|
71
|
+
// A late read cannot move private context into a new account/project.
|
|
72
|
+
const currentToken = await deps.resolveToken();
|
|
73
|
+
if (!currentToken || scope !== reviewScope(ctx.sessionManager.getSessionId(), deps.getBond()?.projectId ?? ctx.cwd, currentToken.token)) return undefined;
|
|
74
|
+
if (!isError) {
|
|
75
|
+
observeReviewResult("search_memories", details, { query }, scope);
|
|
76
|
+
const cards = extractCards(details);
|
|
77
|
+
const agreed = cards.filter(isAgreed);
|
|
78
|
+
const fresh = unseenCards(agreed, injected);
|
|
79
|
+
for (const c of fresh) {
|
|
80
|
+
const k = cardKey(c);
|
|
81
|
+
if (k) injected.add(k);
|
|
82
|
+
}
|
|
83
|
+
if (fresh.length > 0) {
|
|
84
|
+
debugRemote("recall", { queryChars: query.length, newHits: fresh.length });
|
|
85
|
+
const recall = formatRecallBlock(fresh, cards.length - agreed.length);
|
|
86
|
+
noteInjectedContext("recall", recall.length);
|
|
87
|
+
parts.push(recall);
|
|
88
|
+
} else {
|
|
89
|
+
debugRemote("recall", { queryChars: query.length, newHits: 0 });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
debugRemote("recall failed", { errorPresent: Boolean(err) });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 2. Review nudge — only when the open set changed since last inject.
|
|
98
|
+
const flags = listFlags(scope);
|
|
99
|
+
const sig = snapshotHash([scope, flags]);
|
|
100
|
+
if (!flags.length) lastNudgeSig = "";
|
|
101
|
+
if (flags.length > 0 && sig !== lastNudgeSig) {
|
|
102
|
+
lastNudgeSig = sig;
|
|
103
|
+
const nudge = formatReviewNudge(flags);
|
|
104
|
+
if (nudge) {
|
|
105
|
+
noteInjectedContext("conflictNudge", nudge.length);
|
|
106
|
+
parts.push(nudge);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (parts.length === 0) return undefined;
|
|
111
|
+
return {
|
|
112
|
+
message: {
|
|
113
|
+
customType: "devspec-proactive",
|
|
114
|
+
content: parts.join("\n\n"),
|
|
115
|
+
display: false,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
pi.registerCommand("devspec.review", {
|
|
121
|
+
description: "List the open review questions (possibly out of date, or waiting for someone's OK) seen on records in this session's project.",
|
|
122
|
+
handler: async (_args, ctx) => {
|
|
123
|
+
const token = await deps.resolveToken();
|
|
124
|
+
if (!token) return;
|
|
125
|
+
const flags = listFlags(reviewScope(ctx.sessionManager.getSessionId(), deps.getBond()?.projectId ?? ctx.cwd, token.token));
|
|
126
|
+
if (flags.length === 0) {
|
|
127
|
+
ctx.ui.notify("DevSpec: no open review questions seen this session.", "info");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
ctx.ui.notify(
|
|
131
|
+
formatReviewNudge(flags) ?? "No observed Review context.",
|
|
132
|
+
"warning",
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability-honest provenance assistance for Pi.
|
|
3
|
+
*
|
|
4
|
+
* Pi exposes reliable first-class file edit calls, so an explicitly associated
|
|
5
|
+
* project may receive one advisory nudge. This extension never parses shell
|
|
6
|
+
* text, blocks tools, or claims push enforcement. Commit tags are added by Pi's
|
|
7
|
+
* git stand-in when exactly one task is claimed (item df71ab6c), never here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { resolveEndpoint, resolveToken } from "./core/auth-store.ts";
|
|
13
|
+
import { callDevspecAdvisoryOnce } from "./core/client.ts";
|
|
14
|
+
import { resolveGitRemote, resolveProjectPin, resolveProjectPinFile, writeProjectPin } from "./core/project-scope.ts";
|
|
15
|
+
import { getProjectChoice } from "./core/project-choice.ts";
|
|
16
|
+
import { unwrapMcpResult } from "./core/result.ts";
|
|
17
|
+
import { hasObservedClaim, markProjectNudged, unmarkProjectNudged } from "./core/work-state.ts";
|
|
18
|
+
import { commitTaggingAvailable } from "./core/commit-stamp.ts";
|
|
19
|
+
|
|
20
|
+
const RELIABLE_EDIT_TOOLS = new Set(["write", "edit", "apply_patch", "notebook"]);
|
|
21
|
+
const FULL_PROJECT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
22
|
+
const JURISDICTION_CACHE_KEY = "__devspecPiNudgeJurisdiction";
|
|
23
|
+
const JURISDICTION_PENDING_KEY = "__devspecPiNudgeJurisdictionPending";
|
|
24
|
+
const POSITIVE_TTL_MS = 5 * 60 * 1000;
|
|
25
|
+
const NEGATIVE_TTL_MS = 60 * 1000;
|
|
26
|
+
const MAX_CACHE_ENTRIES = 50;
|
|
27
|
+
|
|
28
|
+
export interface ProvenanceRuntime {
|
|
29
|
+
cwd: string;
|
|
30
|
+
sessionManager: { getSessionId(): string };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ProvenanceNudge {
|
|
34
|
+
customType: "devspec-provenance-nudge";
|
|
35
|
+
content: string;
|
|
36
|
+
display: true;
|
|
37
|
+
details: { projectId: string };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface JurisdictionDeps {
|
|
41
|
+
resolveToken(): Promise<{ token: string } | null>;
|
|
42
|
+
resolveEndpoint(): Promise<string>;
|
|
43
|
+
resolveGitRemote(cwd: string): Promise<string | null>;
|
|
44
|
+
resolveProjectPin(cwd: string): Promise<string | null>;
|
|
45
|
+
call(token: string, name: string, args: Record<string, unknown>): Promise<unknown>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface JurisdictionCacheEntry {
|
|
49
|
+
projectId: string | null;
|
|
50
|
+
expiresAt: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function jurisdictionCache(): Map<string, JurisdictionCacheEntry> {
|
|
54
|
+
const g = globalThis as unknown as Record<string, Map<string, JurisdictionCacheEntry> | undefined>;
|
|
55
|
+
if (!(g[JURISDICTION_CACHE_KEY] instanceof Map)) g[JURISDICTION_CACHE_KEY] = new Map();
|
|
56
|
+
return g[JURISDICTION_CACHE_KEY] as Map<string, JurisdictionCacheEntry>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function jurisdictionPending(): Map<string, Promise<string | null>> {
|
|
60
|
+
const g = globalThis as unknown as Record<string, Map<string, Promise<string | null>> | undefined>;
|
|
61
|
+
if (!(g[JURISDICTION_PENDING_KEY] instanceof Map)) g[JURISDICTION_PENDING_KEY] = new Map();
|
|
62
|
+
return g[JURISDICTION_PENDING_KEY] as Map<string, Promise<string | null>>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const jurisdictionDeps: JurisdictionDeps = {
|
|
66
|
+
resolveToken,
|
|
67
|
+
resolveEndpoint,
|
|
68
|
+
resolveGitRemote,
|
|
69
|
+
resolveProjectPin,
|
|
70
|
+
call: (token, name, args) => callDevspecAdvisoryOnce(token, name, args, 1500),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function tokenFingerprint(token: string): string {
|
|
74
|
+
return createHash("sha256").update(token).digest("hex").slice(0, 16);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function remoteMatch(raw: unknown): { resolved: string | null; candidates: string[] } | null {
|
|
78
|
+
const result = unwrapMcpResult(raw);
|
|
79
|
+
if (result.isError || !result.details || typeof result.details !== "object") return null;
|
|
80
|
+
const match = (result.details as { remote_match?: unknown }).remote_match;
|
|
81
|
+
if (!match || typeof match !== "object") return null;
|
|
82
|
+
const row = match as { resolved_project_id?: unknown; candidate_project_ids?: unknown };
|
|
83
|
+
const resolved = typeof row.resolved_project_id === "string" ? row.resolved_project_id : null;
|
|
84
|
+
const candidates = Array.isArray(row.candidate_project_ids)
|
|
85
|
+
? row.candidate_project_ids.filter((id): id is string => typeof id === "string")
|
|
86
|
+
: [];
|
|
87
|
+
return { resolved, candidates };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Resolve positive project jurisdiction without writing the folder or guessing. */
|
|
91
|
+
export async function resolveNudgeProject(
|
|
92
|
+
cwd: string,
|
|
93
|
+
deps: JurisdictionDeps = jurisdictionDeps,
|
|
94
|
+
now = Date.now(),
|
|
95
|
+
): Promise<string | null> {
|
|
96
|
+
const [pin, remote] = await Promise.all([
|
|
97
|
+
deps.resolveProjectPin(cwd).catch(() => null),
|
|
98
|
+
deps.resolveGitRemote(cwd).catch(() => null),
|
|
99
|
+
]);
|
|
100
|
+
const validPin = typeof pin === "string" && FULL_PROJECT_ID.test(pin) ? pin : null;
|
|
101
|
+
if (!remote) return validPin;
|
|
102
|
+
|
|
103
|
+
const [auth, endpoint] = await Promise.all([
|
|
104
|
+
deps.resolveToken().catch(() => null),
|
|
105
|
+
deps.resolveEndpoint().catch(() => ""),
|
|
106
|
+
]);
|
|
107
|
+
if (!auth?.token || !endpoint) return null;
|
|
108
|
+
const key = `${endpoint}\0${tokenFingerprint(auth.token)}\0${remote}\0${validPin ?? ""}`;
|
|
109
|
+
const cache = jurisdictionCache();
|
|
110
|
+
const hit = cache.get(key);
|
|
111
|
+
if (hit && hit.expiresAt > now) return hit.projectId;
|
|
112
|
+
|
|
113
|
+
const pending = jurisdictionPending();
|
|
114
|
+
const existing = pending.get(key);
|
|
115
|
+
if (existing) return existing;
|
|
116
|
+
if (pending.size >= MAX_CACHE_ENTRIES) return null;
|
|
117
|
+
|
|
118
|
+
const lookup = (async (): Promise<string | null> => {
|
|
119
|
+
let projectId: string | null = null;
|
|
120
|
+
try {
|
|
121
|
+
const raw = await deps.call(auth.token, "list_projects", { git_remote: remote });
|
|
122
|
+
const match = remoteMatch(raw);
|
|
123
|
+
if (match?.resolved && FULL_PROJECT_ID.test(match.resolved)) {
|
|
124
|
+
projectId = match.resolved;
|
|
125
|
+
} else if (validPin && match?.candidates.includes(validPin)) {
|
|
126
|
+
projectId = validPin;
|
|
127
|
+
} else if (validPin && match && match.candidates.length === 0) {
|
|
128
|
+
projectId = validPin;
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
projectId = null;
|
|
132
|
+
}
|
|
133
|
+
cache.set(key, { projectId, expiresAt: now + (projectId ? POSITIVE_TTL_MS : NEGATIVE_TTL_MS) });
|
|
134
|
+
while (cache.size > MAX_CACHE_ENTRIES) {
|
|
135
|
+
const oldest = cache.keys().next().value;
|
|
136
|
+
if (!oldest) break;
|
|
137
|
+
cache.delete(oldest);
|
|
138
|
+
}
|
|
139
|
+
return projectId;
|
|
140
|
+
})().finally(() => pending.delete(key));
|
|
141
|
+
pending.set(key, lookup);
|
|
142
|
+
return lookup;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function resetNudgeJurisdictionCacheForTests(): void {
|
|
146
|
+
jurisdictionCache().clear();
|
|
147
|
+
jurisdictionPending().clear();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function unclaimedEditNudge(
|
|
151
|
+
toolName: string,
|
|
152
|
+
runtime: ProvenanceRuntime,
|
|
153
|
+
projectResolver: (cwd: string) => Promise<string | null> = resolveNudgeProject,
|
|
154
|
+
commitTagging: () => boolean = commitTaggingAvailable,
|
|
155
|
+
): Promise<ProvenanceNudge | null> {
|
|
156
|
+
if (!RELIABLE_EDIT_TOOLS.has(toolName)) return null;
|
|
157
|
+
const sessionId = runtime.sessionManager.getSessionId();
|
|
158
|
+
|
|
159
|
+
// Positive jurisdiction is either an explicit pin or a server-verified remote.
|
|
160
|
+
// Every auth/network/ambiguous/malformed state is a plain non-blocking null.
|
|
161
|
+
const projectId = getProjectChoice(sessionId)?.project.id ?? await projectResolver(runtime.cwd);
|
|
162
|
+
if (!projectId || !FULL_PROJECT_ID.test(projectId) || hasObservedClaim(sessionId, projectId) || !markProjectNudged(sessionId, projectId)) return null;
|
|
163
|
+
return {
|
|
164
|
+
customType: "devspec-provenance-nudge",
|
|
165
|
+
content:
|
|
166
|
+
`DevSpec provenance note: this conversation is working with project ${projectId}, ` +
|
|
167
|
+
"but this Pi session has not observed an active claim. Continue this edit—editing is never blocked. " +
|
|
168
|
+
"Before committing, use search_tasks to find and claim the covering item. If none exists, " +
|
|
169
|
+
"create the smallest task that does, with a substantive description and the reason for the work (create_task " +
|
|
170
|
+
"requires both), as the implementation contract's work_entry_contract decides, and claim it. " +
|
|
171
|
+
(commitTagging()
|
|
172
|
+
? "Once one task is claimed, Pi adds its [devspec:<full-uuid>] tag to your commits itself; write the tag yourself only when Pi says it did not."
|
|
173
|
+
: "Pi cannot add task tags to commits in this setup, so put the claimed task's [devspec:<full-uuid>] in every commit message yourself."),
|
|
174
|
+
display: true,
|
|
175
|
+
details: { projectId },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export default function (pi: ExtensionAPI): void {
|
|
180
|
+
let active = true;
|
|
181
|
+
pi.on("session_shutdown", () => { active = false; });
|
|
182
|
+
// Work observations live on globalThis and are keyed by Pi session id, so a
|
|
183
|
+
// /reload cannot re-arm a successfully delivered nudge or forget a claim.
|
|
184
|
+
pi.on("tool_call", (event, ctx) => {
|
|
185
|
+
// Pi fail-safes any synchronous throw by blocking the tool. Session capture,
|
|
186
|
+
// detached launch, network lookup and delivery therefore all live inside
|
|
187
|
+
// explicit fail-open handling, and this handler always returns undefined.
|
|
188
|
+
try {
|
|
189
|
+
const originatingSessionId = ctx.sessionManager.getSessionId();
|
|
190
|
+
void (async () => {
|
|
191
|
+
let nudge: ProvenanceNudge | null = null;
|
|
192
|
+
try {
|
|
193
|
+
nudge = await unclaimedEditNudge(event.toolName, ctx);
|
|
194
|
+
if (!nudge) return;
|
|
195
|
+
if (!active || ctx.sessionManager.getSessionId() !== originatingSessionId) {
|
|
196
|
+
unmarkProjectNudged(originatingSessionId, nudge.details.projectId);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
pi.sendMessage(nudge, { deliverAs: "steer", triggerTurn: false });
|
|
200
|
+
} catch {
|
|
201
|
+
if (nudge) unmarkProjectNudged(originatingSessionId, nudge.details.projectId);
|
|
202
|
+
}
|
|
203
|
+
})();
|
|
204
|
+
} catch { /* advisory-only */ }
|
|
205
|
+
return undefined;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
pi.registerCommand("devspec.pin", {
|
|
209
|
+
description: "Explicitly associate this repository/folder with a DevSpec project id.",
|
|
210
|
+
handler: async (args, ctx) => {
|
|
211
|
+
const projectId = args.trim();
|
|
212
|
+
if (!FULL_PROJECT_ID.test(projectId)) {
|
|
213
|
+
ctx.ui.notify("Usage: /devspec.pin <full-project-uuid>", "warning");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const existing = await resolveProjectPinFile(ctx.cwd);
|
|
218
|
+
if (existing && existing.projectId !== projectId) {
|
|
219
|
+
if (ctx.mode !== "tui" || !await ctx.ui.confirm("Replace folder default?", `${existing.projectId} at ${existing.path}\nNew project: ${projectId}\nThis may affect other users of the folder, but not existing conversations.`)) return;
|
|
220
|
+
if (JSON.stringify(await resolveProjectPinFile(ctx.cwd)) !== JSON.stringify(existing)) {
|
|
221
|
+
ctx.ui.notify("The folder default changed. Review it again before replacing it.", "warning"); return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const result = await writeProjectPin(ctx.cwd, projectId);
|
|
225
|
+
const replacement = result.replaced ? ` (replaced ${result.replaced})` : "";
|
|
226
|
+
ctx.ui.notify(`DevSpec project association written to ${result.path}${replacement}.`, "info");
|
|
227
|
+
} catch (error) {
|
|
228
|
+
// Project association assists local scoping; failure never blocks work.
|
|
229
|
+
ctx.ui.notify(`DevSpec could not write the project association: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
}
|