@mgiles/perk 3.1.0 → 3.3.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 +5 -0
- package/extension/authoring/gist/draft.ts +198 -0
- package/extension/authoring/gist/prose.ts +46 -0
- package/extension/authoring/gist/review.ts +133 -0
- package/extension/authoring/gist/save.ts +118 -0
- package/extension/authoring/objective/draft.ts +345 -0
- package/extension/{factories/objectiveDreamReport.ts → authoring/objective/dreamReportGate.ts} +74 -131
- package/extension/authoring/objective/planning.ts +124 -0
- package/extension/authoring/objective/prose.ts +103 -0
- package/extension/authoring/objective/review.ts +128 -0
- package/extension/authoring/objective/save.ts +224 -0
- package/extension/authoring/plan/draft.ts +84 -0
- package/extension/authoring/plan/prose.ts +41 -0
- package/extension/authoring/plan/review.ts +269 -0
- package/extension/authoring/plan/save.ts +256 -0
- package/extension/authoring/plan/source.ts +82 -0
- package/extension/authoring/refinement/context.ts +468 -0
- package/extension/authoring/refinement/draft.ts +261 -0
- package/extension/authoring/refinement/prose.ts +79 -0
- package/extension/authoring/refinement/review.ts +111 -0
- package/extension/authoring/refinement/save.ts +119 -0
- package/extension/authoring/review/approvalGate.ts +34 -0
- package/extension/authoring/review/draftContext.ts +68 -0
- package/extension/codeReview/automated.ts +352 -0
- package/extension/codeReview/submission.ts +229 -0
- package/extension/delivery/address.ts +295 -0
- package/extension/delivery/ci.ts +355 -0
- package/extension/delivery/commitCompact.ts +93 -0
- package/extension/delivery/conflictResolution.ts +247 -0
- package/extension/delivery/ready.ts +193 -0
- package/extension/delivery/stackConflict.ts +361 -0
- package/extension/delivery/stackObjective.ts +16 -0
- package/extension/delivery/stackReconcile.ts +165 -0
- package/extension/delivery/submit.ts +171 -0
- package/extension/index.ts +369 -359
- package/extension/learning/analystWave.ts +324 -0
- package/extension/learning/audit.ts +667 -0
- package/extension/learning/capture.ts +92 -0
- package/extension/learning/containment.ts +104 -0
- package/extension/{waves/dreamWave.ts → learning/dream.ts} +112 -94
- package/extension/learning/dreamAnalysis.ts +435 -0
- package/extension/{waves/dreamReducerWave.ts → learning/dreamReducer.ts} +46 -41
- package/extension/{waves → learning}/dreamReport.ts +35 -31
- package/extension/learning/harvest.ts +491 -0
- package/extension/learning/prose.ts +66 -0
- package/extension/learning/routing.ts +79 -0
- package/extension/pi/v1/bashScanTimeout.ts +64 -0
- package/extension/{doors/prReview.ts → pi/v1/codeReview/automated.ts} +215 -310
- package/extension/{doors/prReviewBrowser.ts → pi/v1/codeReview/browser.ts} +125 -57
- package/extension/{doors/hunkHandoff.ts → pi/v1/codeReview/checkout.ts} +12 -8
- package/extension/{doors/reviewWaveTools.ts → pi/v1/codeReview/reviewWave.ts} +169 -116
- package/extension/pi/v1/codeReview/stack.ts +606 -0
- package/extension/pi/v1/codeReview/submit.ts +354 -0
- package/extension/{doors/prReviewTerminal.ts → pi/v1/codeReview/terminal.ts} +32 -27
- package/extension/pi/v1/contextEvidence.ts +80 -0
- package/extension/pi/v1/contextInjection.ts +207 -0
- package/extension/{doors → pi/v1/delivery}/address.ts +163 -265
- package/extension/pi/v1/delivery/ci.ts +570 -0
- package/extension/pi/v1/delivery/commitCompact.ts +201 -0
- package/extension/pi/v1/delivery/conflictResolverEngine.ts +425 -0
- package/extension/{doors → pi/v1/delivery}/land.ts +128 -60
- package/extension/pi/v1/delivery/ready.ts +322 -0
- package/extension/pi/v1/delivery/stackConflictResolver.ts +172 -0
- package/extension/pi/v1/delivery/stackDrive.ts +120 -0
- package/extension/pi/v1/delivery/stackLand.ts +223 -0
- package/extension/pi/v1/delivery/stackRecover.ts +265 -0
- package/extension/pi/v1/delivery/stackStatus.ts +237 -0
- package/extension/pi/v1/delivery/stackSync.ts +658 -0
- package/extension/pi/v1/delivery/submit.ts +389 -0
- package/extension/pi/v1/delivery/submitConflict.ts +186 -0
- package/extension/pi/v1/draftReview.ts +431 -0
- package/extension/{doors → pi/v1}/draftReviewWaveTools.ts +141 -151
- package/extension/pi/v1/gist.ts +794 -0
- package/extension/pi/v1/learning/audit.ts +186 -0
- package/extension/pi/v1/learning/dream.ts +207 -0
- package/extension/{doors/learnFactory.ts → pi/v1/learning/factory.ts} +18 -65
- package/extension/{doors/harvestWaveTools.ts → pi/v1/learning/harvest.ts} +46 -100
- package/extension/pi/v1/learning/learn.ts +585 -0
- package/extension/{doors → pi/v1}/lifecycleGates.ts +14 -59
- package/extension/{factories → pi/v1}/objective.ts +53 -33
- package/extension/pi/v1/objectiveAuthoring.ts +672 -0
- package/extension/pi/v1/objectiveDreamGate.ts +160 -0
- package/extension/pi/v1/objectivePlanning.ts +776 -0
- package/extension/pi/v1/objectiveRefinement.ts +1320 -0
- package/extension/pi/v1/objectiveReview.ts +451 -0
- package/extension/{doors → pi/v1}/objectiveReviewBrowser.ts +259 -172
- package/extension/pi/v1/plan.ts +812 -0
- package/extension/pi/v1/planReview.ts +820 -0
- package/extension/{doors → pi/v1}/planReviewBrowser.ts +228 -152
- package/extension/{doors/annotationPush.ts → pi/v1/providers/annotations.ts} +158 -89
- package/extension/pi/v1/providers/plannotator.ts +487 -0
- package/extension/{doors → pi/v1/providers}/plannotatorHandoff.ts +152 -34
- package/extension/pi/v1/providers/selection.ts +43 -0
- package/extension/{adapters/planAdapterTombell.ts → pi/v1/providers/tombell.ts} +43 -72
- package/extension/pi/v1/review.ts +538 -0
- package/extension/pi/v1/reviewOutcome.ts +9 -0
- package/extension/pi/v1/scoutWave.ts +318 -0
- package/extension/{doors → pi/v1}/selfcheck.ts +4 -4
- package/extension/session/branchWorkflowSession.ts +60 -0
- package/extension/session/lifecycle.ts +644 -0
- package/extension/session/lifecycleGates.ts +64 -0
- package/extension/session/saveDestination.ts +87 -0
- package/extension/session/workflowSession.ts +971 -0
- package/extension/substrate/agentScratch.ts +27 -54
- package/extension/substrate/bashScanTimeout.ts +181 -0
- package/extension/substrate/bindingDelivery.ts +38 -30
- package/extension/substrate/bindings.ts +4 -5
- package/extension/substrate/cache.ts +64 -12
- package/extension/substrate/childRestrictions.ts +39 -0
- package/extension/substrate/coldDoor.ts +17 -1
- package/extension/substrate/config.ts +157 -19
- package/extension/substrate/git.ts +88 -6
- package/extension/substrate/modelVisible.ts +53 -0
- package/extension/substrate/paths.ts +2 -7
- package/extension/substrate/prompts.ts +22 -0
- package/extension/substrate/registry.ts +2 -0
- package/extension/substrate/resolverLease.ts +364 -0
- package/extension/substrate/sessionData.ts +85 -152
- package/extension/substrate/toolGating.ts +279 -84
- package/extension/substrate/unifiedDiff.ts +1 -1
- package/extension/substrate/workflowState.ts +191 -166
- package/extension/substrate/worktreeResolverLock.ts +261 -0
- package/extension/surfaces/surfaces.ts +79 -27
- package/extension/waves/adversarialReviewWave.ts +103 -48
- package/extension/waves/blockedReports.ts +59 -0
- package/extension/waves/draftReviewWave.ts +42 -42
- package/extension/waves/laneIdentity.ts +77 -0
- package/extension/waves/objectiveExplorerWave.ts +24 -24
- package/extension/waves/prReviewWave.ts +89 -77
- package/extension/waves/reportWave.ts +438 -578
- package/extension/waves/reviewClassifierWave.ts +22 -22
- package/extension/waves/rpcAdapter.ts +100 -15
- package/extension/waves/scoutWave.ts +192 -0
- package/extension/waves/transport.ts +480 -0
- package/extension/worker/sdkAdapter.ts +494 -0
- package/extension/worker/stageExecution.ts +679 -0
- package/extension/workerMain.ts +18 -19
- package/package.json +6 -4
- package/prompts/_fixtures/live.yaml +98 -10
- package/prompts/contexts/adapters/plannotator-gist.md +6 -0
- package/prompts/contexts/adapters/plannotator-objective.md +6 -0
- package/prompts/contexts/adapters/plannotator-plan.md +8 -1
- package/prompts/contexts/adapters/plannotator-refinement.md +22 -0
- package/prompts/contexts/adapters/tombell-plan.md +4 -0
- package/prompts/contexts/objective-refinement.md +17 -0
- package/prompts/contexts/plan-authoring.md +6 -5
- package/prompts/contexts/read-only.md +1 -1
- package/prompts/stages/conflict-resolution-continuation.md +9 -0
- package/prompts/stages/conflict-resolution.md +4 -4
- package/prompts/stages/objective-author/adopt.md +1 -1
- package/prompts/stages/objective-author/file.md +1 -1
- package/prompts/stages/objective-author/seed.md +1 -1
- package/prompts/stages/objective-plan/guidance.md +2 -2
- package/prompts/stages/objective-plan/seed.md +9 -1
- package/prompts/stages/objective-reconcile-ready.md +7 -0
- package/prompts/stages/objective-reconcile.md +1 -1
- package/prompts/stages/objective-refine/seed.md +18 -0
- package/prompts/stages/objective-review-browser.md +4 -4
- package/prompts/stages/objective-sync.md +1 -1
- package/prompts/stages/plan-review-browser.md +4 -4
- package/prompts/stages/pr-review-browser/active.md +3 -4
- package/prompts/stages/pr-review-browser/foreign.md +3 -4
- package/prompts/stages/pr-review-terminal/active.md +3 -3
- package/prompts/stages/pr-review-terminal/foreign.md +3 -3
- package/prompts/stages/pr-review.md +3 -3
- package/prompts/stages/stack-review/cold.md +1 -0
- package/prompts/stages/stack-review-browser/stack.md +22 -0
- package/shared/README.md +8 -3
- package/shared/bindings.yaml +6 -3
- package/shared/contracts.md +4499 -2147
- package/shared/fixtures/issues-table.json +130 -0
- package/shared/registry.yaml +29 -1
- package/shared/schemas/outputs/objective-node-engagement.schema.json +318 -0
- package/shared/schemas/outputs/objective-stack-status.schema.json +178 -2
- package/shared/schemas/outputs/pr-ready.schema.json +110 -2
- package/shared/schemas/outputs/pr-review-context.schema.json +54 -9
- package/shared/schemas/outputs/pr-review-stack-context.schema.json +196 -0
- package/extension/adapters/planAdapterPlannotator.ts +0 -362
- package/extension/doors/auditWaveTools.ts +0 -352
- package/extension/doors/ciExecutor.ts +0 -756
- package/extension/doors/commitCompact.ts +0 -251
- package/extension/doors/dreamWaveTools.ts +0 -475
- package/extension/doors/learn.ts +0 -655
- package/extension/doors/objectiveStack.ts +0 -1143
- package/extension/doors/prReviewDynamic.ts +0 -276
- package/extension/doors/ready.ts +0 -87
- package/extension/doors/submit.ts +0 -347
- package/extension/doors/submitPrReview.ts +0 -408
- package/extension/factories/gistAuthor.ts +0 -94
- package/extension/factories/gistDraft.ts +0 -265
- package/extension/factories/gistSave.ts +0 -251
- package/extension/factories/implementHere.ts +0 -116
- package/extension/factories/objectiveAuthor.ts +0 -98
- package/extension/factories/objectiveDraft.ts +0 -466
- package/extension/factories/objectivePlan.ts +0 -975
- package/extension/factories/objectiveSave.ts +0 -363
- package/extension/factories/planDraft.ts +0 -140
- package/extension/factories/planMode.ts +0 -205
- package/extension/factories/planReview.ts +0 -1237
- package/extension/factories/planSave.ts +0 -604
- package/extension/factories/planTitle.ts +0 -141
- package/extension/substrate/structuredOutput.ts +0 -202
- package/extension/waves/auditWave.ts +0 -312
- package/extension/waves/harvestWave.ts +0 -399
- package/extension/waves/learnWave.ts +0 -155
- package/extension/waves/memoryAdapter.ts +0 -139
- package/extension/waves/prReviewDynamicWave.ts +0 -777
- package/extension/worker/readOnlySession.ts +0 -294
- package/extension/worker/worker.ts +0 -899
- package/prompts/stages/pr-review-dynamic.md +0 -7
- package/shared/contracts-history.md +0 -605
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// Participating submit/address writers exclude each other by canonical worktree Git identity.
|
|
2
|
+
// There is NO reclamation policy. Death, age, reload and cancellation cannot prove quiescence.
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import {
|
|
5
|
+
closeSync,
|
|
6
|
+
constants,
|
|
7
|
+
fstatSync,
|
|
8
|
+
fsyncSync,
|
|
9
|
+
lstatSync,
|
|
10
|
+
openSync,
|
|
11
|
+
readSync,
|
|
12
|
+
type Stats,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { worktreeGitDir } from "./git.ts";
|
|
18
|
+
|
|
19
|
+
const OWNER_LIMIT = 16 * 1024;
|
|
20
|
+
export const WORKTREE_RESOLVER_LOCK = "perk-submit-conflict.lock";
|
|
21
|
+
|
|
22
|
+
interface LockRecord {
|
|
23
|
+
schema: 1;
|
|
24
|
+
token: string;
|
|
25
|
+
pid: number;
|
|
26
|
+
parentSessionId: string;
|
|
27
|
+
ownerRunId: string;
|
|
28
|
+
requestId: string;
|
|
29
|
+
worktreeIdentity: string;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
}
|
|
32
|
+
export type LockOwner = Omit<LockRecord, "token" | "schema">;
|
|
33
|
+
export type LockFinish = {
|
|
34
|
+
kind: "released" | "retained" | "ownership-error" | "io-error";
|
|
35
|
+
path: string;
|
|
36
|
+
};
|
|
37
|
+
export interface WorktreeResolverClaim {
|
|
38
|
+
readonly path: string;
|
|
39
|
+
check(): "owned" | "ownership-error" | "io-error";
|
|
40
|
+
/** First choice wins, locally idempotent. Retain closes resources but never removes metadata. */
|
|
41
|
+
finish(disposition: "release" | "retain"): LockFinish;
|
|
42
|
+
}
|
|
43
|
+
export type WorktreeResolverAcquisition =
|
|
44
|
+
| { kind: "acquired"; claim: WorktreeResolverClaim }
|
|
45
|
+
| { kind: "unavailable" }
|
|
46
|
+
| { kind: "busy"; path: string; owner?: LockOwner }
|
|
47
|
+
| { kind: "io-error"; path: string; residue: boolean };
|
|
48
|
+
|
|
49
|
+
/** Only deterministic filesystem fault tests substitute these operations. */
|
|
50
|
+
export interface WorktreeLockFs {
|
|
51
|
+
open: typeof openSync;
|
|
52
|
+
close: typeof closeSync;
|
|
53
|
+
fstat: (fd: number) => Stats;
|
|
54
|
+
lstat: (path: string) => Stats;
|
|
55
|
+
read: (fd: number, buffer: Buffer, offset: number, length: number, position: number) => number;
|
|
56
|
+
write: (fd: number, data: string) => void;
|
|
57
|
+
sync: (fd: number) => void;
|
|
58
|
+
unlink: (path: string) => void;
|
|
59
|
+
}
|
|
60
|
+
const realFs: WorktreeLockFs = {
|
|
61
|
+
open: openSync,
|
|
62
|
+
close: closeSync,
|
|
63
|
+
fstat: fstatSync,
|
|
64
|
+
lstat: lstatSync,
|
|
65
|
+
read: readSync,
|
|
66
|
+
write: (fd, data) => writeFileSync(fd, data, "utf8"),
|
|
67
|
+
sync: fsyncSync,
|
|
68
|
+
unlink: unlinkSync,
|
|
69
|
+
};
|
|
70
|
+
function code(error: unknown): unknown {
|
|
71
|
+
return typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
|
|
72
|
+
}
|
|
73
|
+
function sameFile(a: Stats, b: Stats): boolean {
|
|
74
|
+
return a.isFile() && b.isFile() && a.dev === b.dev && a.ino === b.ino;
|
|
75
|
+
}
|
|
76
|
+
function readRecord(fs: WorktreeLockFs, fd: number): LockRecord | null {
|
|
77
|
+
const size = fs.fstat(fd).size;
|
|
78
|
+
if (size <= 0 || size > OWNER_LIMIT) return null;
|
|
79
|
+
const bytes = Buffer.alloc(size);
|
|
80
|
+
let read = 0;
|
|
81
|
+
while (read < size) {
|
|
82
|
+
const n = fs.read(fd, bytes, read, size - read, read);
|
|
83
|
+
if (n === 0) return null;
|
|
84
|
+
read += n;
|
|
85
|
+
}
|
|
86
|
+
let raw: unknown;
|
|
87
|
+
try {
|
|
88
|
+
raw = JSON.parse(bytes.toString("utf8"));
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
93
|
+
const r = raw as Record<string, unknown>;
|
|
94
|
+
if (
|
|
95
|
+
r.schema !== 1 ||
|
|
96
|
+
typeof r.token !== "string" ||
|
|
97
|
+
!r.token ||
|
|
98
|
+
typeof r.pid !== "number" ||
|
|
99
|
+
!Number.isInteger(r.pid) ||
|
|
100
|
+
r.pid <= 0
|
|
101
|
+
)
|
|
102
|
+
return null;
|
|
103
|
+
for (const field of [
|
|
104
|
+
"parentSessionId",
|
|
105
|
+
"ownerRunId",
|
|
106
|
+
"requestId",
|
|
107
|
+
"worktreeIdentity",
|
|
108
|
+
"createdAt",
|
|
109
|
+
] as const) {
|
|
110
|
+
if (typeof r[field] !== "string" || !r[field]) return null;
|
|
111
|
+
}
|
|
112
|
+
return r as unknown as LockRecord;
|
|
113
|
+
}
|
|
114
|
+
function ownerOf(r: LockRecord): LockOwner {
|
|
115
|
+
return {
|
|
116
|
+
pid: r.pid,
|
|
117
|
+
parentSessionId: r.parentSessionId,
|
|
118
|
+
ownerRunId: r.ownerRunId,
|
|
119
|
+
requestId: r.requestId,
|
|
120
|
+
worktreeIdentity: r.worktreeIdentity,
|
|
121
|
+
createdAt: r.createdAt,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function incumbent(fs: WorktreeLockFs, path: string): WorktreeResolverAcquisition {
|
|
126
|
+
let fd: number | undefined;
|
|
127
|
+
try {
|
|
128
|
+
const stat = fs.lstat(path);
|
|
129
|
+
if (!stat.isFile() || stat.size > OWNER_LIMIT) return { kind: "busy", path };
|
|
130
|
+
fd = fs.open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
131
|
+
if (!sameFile(stat, fs.fstat(fd))) return { kind: "busy", path };
|
|
132
|
+
const record = readRecord(fs, fd);
|
|
133
|
+
return { kind: "busy", path, ...(record ? { owner: ownerOf(record) } : {}) };
|
|
134
|
+
} catch (error) {
|
|
135
|
+
// Even a disappearing incumbent doesn't invite a retry. A racing external replacement is
|
|
136
|
+
// outside the protocol; ELOOP never follows the new symlink for diagnostic reads.
|
|
137
|
+
if (code(error) === "ENOENT" || code(error) === "ELOOP") return { kind: "busy", path };
|
|
138
|
+
return { kind: "io-error", path, residue: true };
|
|
139
|
+
} finally {
|
|
140
|
+
if (fd !== undefined) fs.close(fd);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function acquireWorktreeResolverLock(
|
|
145
|
+
cwd: string,
|
|
146
|
+
parent: { sessionId: string; runId: string; requestId: string },
|
|
147
|
+
opts: {
|
|
148
|
+
fs?: Partial<WorktreeLockFs>;
|
|
149
|
+
gitDir?: (cwd: string) => string | null;
|
|
150
|
+
/** The token minter (default `randomUUID`) — only deterministic fault tests substitute it. */
|
|
151
|
+
token?: () => string;
|
|
152
|
+
} = {},
|
|
153
|
+
): WorktreeResolverAcquisition {
|
|
154
|
+
const identity = (opts.gitDir ?? worktreeGitDir)(cwd);
|
|
155
|
+
if (identity === null) return { kind: "unavailable" };
|
|
156
|
+
const path = join(identity, WORKTREE_RESOLVER_LOCK);
|
|
157
|
+
const fs = { ...realFs, ...opts.fs };
|
|
158
|
+
let fd: number;
|
|
159
|
+
try {
|
|
160
|
+
fd = fs.open(path, "wx", 0o600);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (code(error) === "EEXIST") {
|
|
163
|
+
try {
|
|
164
|
+
return incumbent(fs, path);
|
|
165
|
+
} catch {
|
|
166
|
+
return { kind: "io-error", path, residue: true };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { kind: "io-error", path, residue: false };
|
|
170
|
+
}
|
|
171
|
+
// Everything after the exclusive create runs inside the cleanup-protected block — the record
|
|
172
|
+
// construction included: a token-minting failure must take the same identity-fenced unlink
|
|
173
|
+
// path as a write failure, never escape with the descriptor open and an empty lock file that
|
|
174
|
+
// would wedge every later acquisition as busy.
|
|
175
|
+
let stat: Stats | undefined;
|
|
176
|
+
let token: string;
|
|
177
|
+
try {
|
|
178
|
+
stat = fs.fstat(fd);
|
|
179
|
+
if (!stat.isFile()) throw new Error("not a regular file");
|
|
180
|
+
token = (opts.token ?? randomUUID)();
|
|
181
|
+
const record: LockRecord = {
|
|
182
|
+
schema: 1,
|
|
183
|
+
token,
|
|
184
|
+
pid: process.pid,
|
|
185
|
+
parentSessionId: parent.sessionId,
|
|
186
|
+
ownerRunId: parent.runId,
|
|
187
|
+
requestId: parent.requestId,
|
|
188
|
+
worktreeIdentity: identity,
|
|
189
|
+
createdAt: new Date().toISOString(),
|
|
190
|
+
};
|
|
191
|
+
fs.write(fd, `${JSON.stringify(record)}\n`);
|
|
192
|
+
fs.sync(fd);
|
|
193
|
+
} catch {
|
|
194
|
+
let residue = true;
|
|
195
|
+
try {
|
|
196
|
+
if (stat && sameFile(stat, fs.lstat(path))) {
|
|
197
|
+
fs.unlink(path);
|
|
198
|
+
residue = false;
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
/* Report residue; never remove an unidentified or successor file. */
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
fs.close(fd);
|
|
205
|
+
} catch {
|
|
206
|
+
residue = true;
|
|
207
|
+
}
|
|
208
|
+
return { kind: "io-error", path, residue };
|
|
209
|
+
}
|
|
210
|
+
const acquiredStat = stat;
|
|
211
|
+
let finished: LockFinish | undefined;
|
|
212
|
+
function check(): "owned" | "ownership-error" | "io-error" {
|
|
213
|
+
if (finished) return "ownership-error";
|
|
214
|
+
try {
|
|
215
|
+
if (!sameFile(acquiredStat, fs.lstat(path)) || !sameFile(acquiredStat, fs.fstat(fd)))
|
|
216
|
+
return "ownership-error";
|
|
217
|
+
const reader = fs.open(
|
|
218
|
+
path,
|
|
219
|
+
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
|
|
220
|
+
);
|
|
221
|
+
try {
|
|
222
|
+
if (!sameFile(acquiredStat, fs.fstat(reader))) return "ownership-error";
|
|
223
|
+
return readRecord(fs, reader)?.token === token ? "owned" : "ownership-error";
|
|
224
|
+
} finally {
|
|
225
|
+
fs.close(reader);
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return code(error) === "ENOENT" ? "ownership-error" : "io-error";
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
kind: "acquired",
|
|
233
|
+
claim: {
|
|
234
|
+
path,
|
|
235
|
+
check,
|
|
236
|
+
finish(disposition) {
|
|
237
|
+
if (finished) return finished;
|
|
238
|
+
let kind: LockFinish["kind"] = "retained";
|
|
239
|
+
try {
|
|
240
|
+
if (disposition === "release") {
|
|
241
|
+
const ownership = check();
|
|
242
|
+
if (ownership === "owned") {
|
|
243
|
+
fs.unlink(path);
|
|
244
|
+
kind = "released";
|
|
245
|
+
} else kind = ownership;
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
kind = "io-error";
|
|
249
|
+
} finally {
|
|
250
|
+
try {
|
|
251
|
+
fs.close(fd);
|
|
252
|
+
} catch {
|
|
253
|
+
kind = "io-error";
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
finished = { kind, path };
|
|
257
|
+
return finished;
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
// surfaces module" is surfaces.ts + report.ts for the surfaces guard).
|
|
7
7
|
//
|
|
8
8
|
// Perk status (charter §6 D2): perk presents ONE footer status under the single
|
|
9
|
-
// `perk` slot — the
|
|
10
|
-
//
|
|
11
|
-
// sanctioned). The perk-owned footer (`perkFooter`/`installPerkFooter` below)
|
|
12
|
-
// directly; the `perk` status slot keeps publishing — it is the RPC-visible
|
|
13
|
-
// is an RPC no-op).
|
|
9
|
+
// `perk` slot — the objective segment composed with the ref-counted activity facet
|
|
10
|
+
// (`<objective> · <activity>`, either half optional). The per-feature status slots and widgets
|
|
11
|
+
// are retired (D8 sanctioned). The perk-owned footer (`perkFooter`/`installPerkFooter` below)
|
|
12
|
+
// renders the value directly; the `perk` status slot keeps publishing — it is the RPC-visible
|
|
13
|
+
// surface (setFooter is an RPC no-op).
|
|
14
14
|
|
|
15
15
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
16
16
|
import type { ReportDetailSink } from "./report.ts";
|
|
@@ -35,6 +35,19 @@ export const STATUS_SLOT_PERK = "perk";
|
|
|
35
35
|
// --- footer identity marks (charter §5 / D3: emoji are footer-only identity, 2 cells wide) ---
|
|
36
36
|
export const MARK_OBJECTIVE = "🎯";
|
|
37
37
|
|
|
38
|
+
// --- activity vocabulary (the optional second half of the composed `perk` status) ---
|
|
39
|
+
// Plain text, no emoji (D3 keeps emoji as identity marks): a Perk-owned wait the operator
|
|
40
|
+
// cannot otherwise see.
|
|
41
|
+
export const ACTIVITY_BROWSER_REVIEW = "waiting on browser review";
|
|
42
|
+
/** The slice an activity owner takes: begin a wait with its text, receive the matching `end`. */
|
|
43
|
+
export type ActivitySink = (text: string) => () => void;
|
|
44
|
+
/**
|
|
45
|
+
* The least-capable slice a wiring layer threads toward an activity owner: begin a wait against
|
|
46
|
+
* the live target, nothing else. The full `PerkStatusHandle` stays at the composition root, the
|
|
47
|
+
* objective publisher and the footer.
|
|
48
|
+
*/
|
|
49
|
+
export type ActivityHandle = Pick<PerkStatusHandle, "beginActivity">;
|
|
50
|
+
|
|
38
51
|
// --- glyph vocabulary (charter §5 / D3) — charter-law data, pinned by tests ---
|
|
39
52
|
export type GlyphKind = "done" | "current" | "pending" | "warning" | "failure";
|
|
40
53
|
export const GLYPHS: Record<GlyphKind, { glyph: string; themeColor: string }> = {
|
|
@@ -101,41 +114,80 @@ export function setWorkingMessage(target: WorkingMessageTarget, message?: string
|
|
|
101
114
|
// --- the perk status (charter D2) --------------------------------------------
|
|
102
115
|
|
|
103
116
|
/**
|
|
104
|
-
* The
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
117
|
+
* The composed status handle: the objective publisher sets its half through `set`, activity
|
|
118
|
+
* owners hold ref-counted waits through `beginActivity`, and the handle republishes the single
|
|
119
|
+
* `perk` status slot with the composed value. The footer reads it back via `get` and repaints via
|
|
120
|
+
* `subscribe` — the slot's `setStatus` dual-publish is deliberate (RPC clients see the slot;
|
|
121
|
+
* setFooter is an RPC no-op).
|
|
108
122
|
*/
|
|
109
123
|
export interface PerkStatusHandle {
|
|
110
|
-
/** Set (or clear, with undefined) the
|
|
124
|
+
/** Set (or clear, with undefined) the objective half; publishes the slot. No-op headless. */
|
|
111
125
|
set(target: StandingTarget, text: string | undefined): void;
|
|
112
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* Begin one activity wait: the newest live wait's text shows while any begun wait is unended;
|
|
128
|
+
* returns this wait's `end` (idempotent; inert after a reset — no publish). No-op headless
|
|
129
|
+
* (returns a no-op `end`, records nothing).
|
|
130
|
+
*/
|
|
131
|
+
beginActivity(target: StandingTarget, text: string): () => void;
|
|
132
|
+
/** Reset every wait (session shutdown); a late `end` of a reset wait is inert. No-op headless. */
|
|
133
|
+
clearActivity(target: StandingTarget): void;
|
|
134
|
+
/** The composed value (`<objective> · <activity>`, either half optional; undefined when neither). */
|
|
113
135
|
get(): string | undefined;
|
|
114
136
|
/**
|
|
115
|
-
* Subscribe to publishes: the listener fires after every headful `set
|
|
116
|
-
* calls are full no-ops, so nothing fires). Returns an unsubscribe.
|
|
137
|
+
* Subscribe to publishes: the listener fires after every headful `set`, begin, end and clear
|
|
138
|
+
* (headless calls are full no-ops, so nothing fires). Returns an unsubscribe.
|
|
117
139
|
*/
|
|
118
140
|
subscribe(listener: () => void): () => void;
|
|
119
141
|
}
|
|
120
142
|
|
|
121
143
|
/**
|
|
122
|
-
* Create the
|
|
123
|
-
* and passed to the objective publisher; no hidden module state).
|
|
124
|
-
* (never record
|
|
125
|
-
*
|
|
144
|
+
* Create the composed `perk` status handle (one per extension instance — created in index.ts
|
|
145
|
+
* and passed to the objective publisher and the activity owners; no hidden module state).
|
|
146
|
+
* Headless calls are full no-ops (never record text, so headless-era text can't resurrect in a
|
|
147
|
+
* later headful render). The activity is a SET of live wait tokens, not a set/clear: the browser
|
|
148
|
+
* doors accept a concurrent double-open, so an older wait's settle must not blank a newer one —
|
|
149
|
+
* the text clears only when the LAST wait ends (the newest live wait's text shows meanwhile, so
|
|
150
|
+
* ending one restores the remainder). An ended or reset token is simply absent, so its late `end`
|
|
151
|
+
* finds nothing to remove and publishes nothing — a wait begun after a reset can never be
|
|
152
|
+
* consumed by a stale `end`. No width handling: pi's footer truncates.
|
|
126
153
|
*/
|
|
127
154
|
export function createPerkStatus(): PerkStatusHandle {
|
|
128
|
-
let
|
|
155
|
+
let objective: string | undefined;
|
|
156
|
+
const live = new Set<{ text: string }>(); // insertion-ordered: the last entry is the newest
|
|
129
157
|
const listeners = new Set<() => void>();
|
|
158
|
+
const compose = (): string | undefined => {
|
|
159
|
+
const activity = [...live].at(-1)?.text;
|
|
160
|
+
const halves = [objective, activity].filter((half) => half !== undefined);
|
|
161
|
+
return halves.length === 0 ? undefined : halves.join(" · ");
|
|
162
|
+
};
|
|
163
|
+
const publish = (target: StandingTarget): void => {
|
|
164
|
+
target.ui.setStatus(STATUS_SLOT_PERK, compose());
|
|
165
|
+
for (const listener of listeners) listener();
|
|
166
|
+
};
|
|
130
167
|
return {
|
|
131
168
|
set(target, text) {
|
|
132
169
|
if (!target.hasUI) return;
|
|
133
|
-
|
|
134
|
-
target
|
|
135
|
-
|
|
170
|
+
objective = text;
|
|
171
|
+
publish(target);
|
|
172
|
+
},
|
|
173
|
+
beginActivity(target, text) {
|
|
174
|
+
if (!target.hasUI) return () => {};
|
|
175
|
+
const token = { text };
|
|
176
|
+
live.add(token);
|
|
177
|
+
publish(target);
|
|
178
|
+
return () => {
|
|
179
|
+
// Already ended, or reset by `clearActivity`: nothing to remove, nothing to publish.
|
|
180
|
+
if (!live.delete(token)) return;
|
|
181
|
+
publish(target);
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
clearActivity(target) {
|
|
185
|
+
if (!target.hasUI) return;
|
|
186
|
+
live.clear();
|
|
187
|
+
publish(target);
|
|
136
188
|
},
|
|
137
189
|
get() {
|
|
138
|
-
return
|
|
190
|
+
return compose();
|
|
139
191
|
},
|
|
140
192
|
subscribe(listener) {
|
|
141
193
|
listeners.add(listener);
|
|
@@ -155,7 +207,7 @@ export function createPerkStatus(): PerkStatusHandle {
|
|
|
155
207
|
export interface FooterParts {
|
|
156
208
|
/** e.g. `perk v0.0.1` — standing identity (D7), dim. */
|
|
157
209
|
identity: string;
|
|
158
|
-
/** The
|
|
210
|
+
/** The composed perk status value (`handle.get()`), verbatim — it carries its own 🎯 mark. */
|
|
159
211
|
objective?: string;
|
|
160
212
|
/** Git branch (dim); omitted when not in a repo. */
|
|
161
213
|
branch?: string;
|
|
@@ -329,9 +381,9 @@ export type PerkFooterFactory = (
|
|
|
329
381
|
/**
|
|
330
382
|
* The perk-owned footer factory (charter D2): replaces pi's default footer wholesale with one
|
|
331
383
|
* line in the intended split layout. `render` gathers everything live per call (D10 stateless
|
|
332
|
-
* render): the
|
|
333
|
-
* own `STATUS_SLOT_PERK` — the slot keeps publishing for RPC, but the footer renders the
|
|
334
|
-
* directly), model/cache/context via the deps closures. Reactivity (the D2 contract): repaints on
|
|
384
|
+
* render): the composed status value via the handle, branch/guests via `footerData` (excluding
|
|
385
|
+
* perk's own `STATUS_SLOT_PERK` — the slot keeps publishing for RPC, but the footer renders the
|
|
386
|
+
* value directly), model/cache/context via the deps closures. Reactivity (the D2 contract): repaints on
|
|
335
387
|
* every handle recompose and on branch change; `dispose` detaches both. Lifecycle (pi ≥ 0.84,
|
|
336
388
|
* verified at 0.84.1): `setExtensionFooter` disposes a replaced factory's component, and pi's
|
|
337
389
|
* `resetExtensionUI` restores the built-in footer (disposing this one) on /reload and before
|
|
@@ -412,7 +464,7 @@ export function formatBudgetLine(args: { tokens: number; elapsedMs: number }): s
|
|
|
412
464
|
}
|
|
413
465
|
|
|
414
466
|
// --- display-only transcript entry renderers -----------------------------------------------------
|
|
415
|
-
// The audit §2.3 verdict (docs/design/pi-adoption-audit.md): perk's custom-entry families are
|
|
467
|
+
// The audit §2.3 verdict (docs/design/archive/pi-adoption-audit.md): perk's custom-entry families are
|
|
416
468
|
// display-only. Most render as durable one-line transition markers; report detail is the generic
|
|
417
469
|
// full-diagnostic family and always renders every logical row. Renderer BODIES live here (a
|
|
418
470
|
// transcript renderer IS a rich-UI surface the surfaces module owns); registration is wiring at the
|