@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The minimal TS config port. Mirrors `perk/substrate/config.py`'s overlay: read
|
|
2
2
|
// `.perk/config.toml` (committed) overlaid by `.perk/local.toml` (gitignored, local wins). The only
|
|
3
3
|
// setting consumed today is an optional `[workflow]` plan-authoring addendum, appended into the
|
|
4
|
-
// `perk:plan-context` injection (extension/
|
|
4
|
+
// `perk:plan-context` injection (extension/pi/v1/plan.ts) when present.
|
|
5
5
|
//
|
|
6
6
|
// Deliberately dependency-free: rather than pull a runtime TOML dependency into the published
|
|
7
7
|
// extension, this reads the narrow TOML subset perk actually uses — `[section]` headers +
|
|
@@ -48,9 +48,11 @@ export interface PerkConfig {
|
|
|
48
48
|
/**
|
|
49
49
|
* The agent-keyed `[models.subagents]` table: a per-agent model override for each perk-owned
|
|
50
50
|
* project agent (`pr-reviewer`, `review-classifier`, `objective-explorer`, `conflict-resolver`,
|
|
51
|
-
* `learn-analyst`, `adversarial-reviewer`, `
|
|
51
|
+
* `learn-analyst`, `adversarial-reviewer`, `draft-reviewer`,
|
|
52
52
|
* `harvest-analyst` — consumed by `run_harvest_wave` at execute time — `dream-analyst` and
|
|
53
|
-
* `dream-reducer` — consumed by `run_dream_wave` at execute time —
|
|
53
|
+
* `dream-reducer` — consumed by `run_dream_wave` at execute time — `scout` — the
|
|
54
|
+
* general-purpose read-only analysis lane (task-defined scope), consumed by `run_scout_wave`
|
|
55
|
+
* at execute time — and the
|
|
54
56
|
* dev-only `session-auditor`, whose def is repo-local to perk's own repository
|
|
55
57
|
* (`.pi/agents/perk-dev/session-auditor.md`, never delivered by `perk init`), so the key is
|
|
56
58
|
* dormant in consumer repos). Each configured
|
|
@@ -58,8 +60,12 @@ export interface PerkConfig {
|
|
|
58
60
|
* workflowScript call — a default flowing onto every lane, single-child runs included (as
|
|
59
61
|
* /pr-review does); when a key is absent the agent's frontmatter `model` (in
|
|
60
62
|
* `.pi/agents/perk/<name>.md`; the session-auditor's in its repo-local def) is the default.
|
|
61
|
-
* (`subagents.agentOverrides`
|
|
62
|
-
*
|
|
63
|
+
* (Since pi-subagents 0.64.0, `subagents.agentOverrides` applies its FULL override set to
|
|
64
|
+
* custom/project agents too — a settings override CAN displace a def's frontmatter-pinned
|
|
65
|
+
* `model:` (the pre-0.64 frontmatter-sensitive fill is gone; verified in the installed
|
|
66
|
+
* package source). The inline workflow-level injection remains perk's mechanism on its own
|
|
67
|
+
* merits: the config table is perk-owned (no settings churn), and the spawn-time
|
|
68
|
+
* workflow-level `model` wins over the def-level model however that def model was set.)
|
|
63
69
|
* A value may carry a `:thinking` suffix (`"anthropic/claude-sonnet-4-5:high"`) or be the
|
|
64
70
|
* `"inherit"` sentinel (child inherits the parent session's model) — both resolved by
|
|
65
71
|
* pi-subagents on the injected value (the last-colon segment counts as thinking only when it
|
|
@@ -73,11 +79,11 @@ export interface PerkConfig {
|
|
|
73
79
|
"conflict-resolver"?: string;
|
|
74
80
|
"learn-analyst"?: string;
|
|
75
81
|
"adversarial-reviewer"?: string;
|
|
76
|
-
"review-angle-selector"?: string;
|
|
77
82
|
"draft-reviewer"?: string;
|
|
78
83
|
"harvest-analyst"?: string;
|
|
79
84
|
"dream-analyst"?: string;
|
|
80
85
|
"dream-reducer"?: string;
|
|
86
|
+
scout?: string;
|
|
81
87
|
"session-auditor"?: string;
|
|
82
88
|
};
|
|
83
89
|
/**
|
|
@@ -125,8 +131,8 @@ function unescapeBasic(raw: string): string {
|
|
|
125
131
|
* Parse the narrow TOML subset perk consumes. Returns `{ tables, arrays }`: `tables` is a
|
|
126
132
|
* `{ section: { key: scalar } }` map (top-level keys under the `""` section); `arrays` is a
|
|
127
133
|
* `{ name: [{ key: scalar }, ...] }` map fed by `[[name]]` array-of-tables. Scalars are quoted
|
|
128
|
-
* strings, native `true`/`false` booleans,
|
|
129
|
-
* this is intentionally NOT a full TOML parser.
|
|
134
|
+
* strings (basic `"…"`/`"""…"""` and literal `'…'`/`'''…'''`), native `true`/`false` booleans,
|
|
135
|
+
* and numeric literals; anything else is skipped — this is intentionally NOT a full TOML parser.
|
|
130
136
|
*/
|
|
131
137
|
export function parseTomlSubset(text: string): TomlSubset {
|
|
132
138
|
const root: Record<string, TomlScalar> = {};
|
|
@@ -168,17 +174,19 @@ export function parseTomlSubset(text: string): TomlSubset {
|
|
|
168
174
|
const value = line.slice(eq + 1).trim();
|
|
169
175
|
if (key === "") continue;
|
|
170
176
|
|
|
171
|
-
// Multi-line basic
|
|
172
|
-
|
|
177
|
+
// Multi-line strings: basic `""" ... """` (escapes honoured) or literal `''' ... '''`
|
|
178
|
+
// (bytes verbatim), possibly spanning lines.
|
|
179
|
+
const multi = value.startsWith('"""') ? '"""' : value.startsWith("'''") ? "'''" : null;
|
|
180
|
+
if (multi) {
|
|
173
181
|
let body = value.slice(3);
|
|
174
|
-
if (body.endsWith(
|
|
182
|
+
if (body.endsWith(multi) && body.length >= 3) {
|
|
175
183
|
body = body.slice(0, -3);
|
|
176
184
|
} else {
|
|
177
185
|
const parts: string[] = [body];
|
|
178
186
|
i++;
|
|
179
187
|
for (; i < lines.length; i++) {
|
|
180
188
|
const raw = lines[i] ?? "";
|
|
181
|
-
const end = raw.indexOf(
|
|
189
|
+
const end = raw.indexOf(multi);
|
|
182
190
|
if (end !== -1) {
|
|
183
191
|
// A bare closing delimiter on its own line contributes no trailing content (so the
|
|
184
192
|
// newline that precedes it is not appended as an empty segment).
|
|
@@ -191,7 +199,7 @@ export function parseTomlSubset(text: string): TomlSubset {
|
|
|
191
199
|
// A leading newline immediately after the opening delimiter is trimmed (TOML rule).
|
|
192
200
|
if (body.startsWith("\n")) body = body.slice(1);
|
|
193
201
|
}
|
|
194
|
-
dest[key] = unescapeBasic(body);
|
|
202
|
+
dest[key] = multi === '"""' ? unescapeBasic(body) : body;
|
|
195
203
|
continue;
|
|
196
204
|
}
|
|
197
205
|
|
|
@@ -202,6 +210,13 @@ export function parseTomlSubset(text: string): TomlSubset {
|
|
|
202
210
|
continue;
|
|
203
211
|
}
|
|
204
212
|
|
|
213
|
+
// Single-line literal string: '...' (no escapes; a trailing inline comment is dropped).
|
|
214
|
+
const literal = value.match(/^'([^']*)'/);
|
|
215
|
+
if (literal) {
|
|
216
|
+
dest[key] = literal[1] ?? "";
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
205
220
|
// Unquoted scalar: strip an inline `#` comment, then read native booleans and numbers.
|
|
206
221
|
const hash = value.indexOf("#");
|
|
207
222
|
const bare = (hash === -1 ? value : value.slice(0, hash)).trim();
|
|
@@ -326,11 +341,11 @@ const SUBAGENT_KEYS = [
|
|
|
326
341
|
"conflict-resolver",
|
|
327
342
|
"learn-analyst",
|
|
328
343
|
"adversarial-reviewer",
|
|
329
|
-
"review-angle-selector",
|
|
330
344
|
"draft-reviewer",
|
|
331
345
|
"harvest-analyst",
|
|
332
346
|
"dream-analyst",
|
|
333
347
|
"dream-reducer",
|
|
348
|
+
"scout",
|
|
334
349
|
// Dev-only: the perk-dev session-audit judgment wave's auditor (dormant in consumer repos).
|
|
335
350
|
"session-auditor",
|
|
336
351
|
] as const;
|
|
@@ -387,12 +402,135 @@ export const GITHUB_ISSUE_BACKEND_ID: IssueBackendId = "github";
|
|
|
387
402
|
* because the TS plane only renders prompts — it never writes canonical issues.
|
|
388
403
|
*/
|
|
389
404
|
export function resolveIssueBackendId(cwd: string): IssueBackendId {
|
|
405
|
+
const backend = resolveIssueDestination(cwd).backend;
|
|
406
|
+
if (backend === "github" || backend === "linear") return backend;
|
|
407
|
+
return GITHUB_ISSUE_BACKEND_ID;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** The committed `[issues]` routing keys — where a save is written and (Linear) which team owns it. */
|
|
411
|
+
export interface IssueDestination {
|
|
412
|
+
backend: string | null;
|
|
413
|
+
team: string | null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* The committed `[issues]` routing as the save-destination fence reads it. `keys` when the
|
|
418
|
+
* document provably spells the table the way the subset reader parses it — `backend`/`team`
|
|
419
|
+
* are then exactly what Python's `tomllib` reads; `document` when parity cannot be proven (a
|
|
420
|
+
* dotted-key / inline-table / quoted-key spelling, an escape, a duplicate header …) — the
|
|
421
|
+
* verbatim text travels alongside the best-effort keys so the fence can widen to every byte that
|
|
422
|
+
* might route the save instead of trusting a read the authoritative parser may not share.
|
|
423
|
+
*/
|
|
424
|
+
export type IssueRouting =
|
|
425
|
+
| ({ kind: "keys" } & IssueDestination)
|
|
426
|
+
| ({ kind: "document"; text: string } & IssueDestination);
|
|
427
|
+
|
|
428
|
+
/** `StrippedStr`'s boundary rule: a string is stripped and a blank one reads as absent. */
|
|
429
|
+
function strippedString(value: TomlScalar | undefined): string | null {
|
|
430
|
+
if (typeof value !== "string") return null;
|
|
431
|
+
const stripped = value.trim();
|
|
432
|
+
return stripped === "" ? null : stripped;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Can the fence trust `parseTomlSubset(text).tables.issues` as exactly what Python's `tomllib`
|
|
437
|
+
* reads for `[issues] backend`/`team`? Proven only when the document spells the table in the one
|
|
438
|
+
* shape the subset reader handles at parity: a single bare `[issues]` header; `backend`/`team`
|
|
439
|
+
* (each at most once, undotted) as plain single-line strings with no backslash (the subset's
|
|
440
|
+
* escape handling is not `tomllib`'s); and no other header or key segment spelling `issues` —
|
|
441
|
+
* dotted keys, inline tables, quoted keys, `[[issues]]` and super-/sub-tables all reach `tomllib`
|
|
442
|
+
* unseen here. Every line the scanner cannot classify answers `false` (widen, never guess).
|
|
443
|
+
*/
|
|
444
|
+
function issueKeysProvable(text: string): boolean {
|
|
445
|
+
const lines = text.split(/\r?\n/);
|
|
446
|
+
let inIssues = false;
|
|
447
|
+
let issuesHeaders = 0;
|
|
448
|
+
const seen = new Set<string>();
|
|
449
|
+
for (let i = 0; i < lines.length; i++) {
|
|
450
|
+
const line = (lines[i] ?? "").trim();
|
|
451
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
452
|
+
|
|
453
|
+
if (line.startsWith("[")) {
|
|
454
|
+
const array = line.match(/^\[\[([^\]]+)\]\]$/);
|
|
455
|
+
const table = array ? null : line.match(/^\[([^\]]+)\]$/);
|
|
456
|
+
const inner = (array?.[1] ?? table?.[1])?.trim();
|
|
457
|
+
// A header shape the subset reader skips (e.g. a trailing comment) would misplace the keys
|
|
458
|
+
// that follow it; quoted segments can spell any key at all.
|
|
459
|
+
if (inner === undefined || /["']/.test(inner)) return false;
|
|
460
|
+
if (table && inner === "issues") {
|
|
461
|
+
issuesHeaders++;
|
|
462
|
+
if (issuesHeaders > 1) return false;
|
|
463
|
+
inIssues = true;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const segments = inner.split(".").map((segment) => segment.trim());
|
|
467
|
+
if (segments.includes("issues")) return false;
|
|
468
|
+
inIssues = false;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const eq = line.indexOf("=");
|
|
473
|
+
if (eq === -1) return false;
|
|
474
|
+
const key = line.slice(0, eq).trim();
|
|
475
|
+
const value = line.slice(eq + 1).trim();
|
|
476
|
+
if (key === "" || /["']/.test(key)) return false;
|
|
477
|
+
const segments = key.split(".").map((segment) => segment.trim());
|
|
478
|
+
if (segments.includes("issues")) return false;
|
|
479
|
+
if (inIssues && (segments[0] === "backend" || segments[0] === "team")) {
|
|
480
|
+
if (segments.length > 1 || seen.has(key)) return false;
|
|
481
|
+
seen.add(key);
|
|
482
|
+
if (!/^("[^"\\]*"|'[^']*')\s*(#.*)?$/.test(value)) return false;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
// Skip a multi-line string body (the subset reader's rule) so its lines are never mistaken
|
|
486
|
+
// for headers or keys.
|
|
487
|
+
const multi = value.startsWith('"""') ? '"""' : value.startsWith("'''") ? "'''" : null;
|
|
488
|
+
if (multi) {
|
|
489
|
+
const body = value.slice(3);
|
|
490
|
+
if (body.endsWith(multi) && body.length >= 3) continue;
|
|
491
|
+
for (i++; i < lines.length; i++) {
|
|
492
|
+
if ((lines[i] ?? "").includes(multi)) break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Read the committed main-checkout `[issues]` routing for the save-destination fence: the
|
|
501
|
+
* `backend`/`team` keys as Python's boundary reads them (stripped, blank → `null`, non-string →
|
|
502
|
+
* `null`) plus whether that read is provably `tomllib`'s (`kind: "keys"`) or the whole document
|
|
503
|
+
* must stand in for it (`kind: "document"`, carrying the verbatim text). Committed
|
|
504
|
+
* `.perk/config.toml` only (never the `local.toml` overlay), anchored to the MAIN checkout like
|
|
505
|
+
* `resolveIssueBackendId`. A missing file or a read failure is the empty table (`keys`,
|
|
506
|
+
* both `null`) — Python reads the same absence.
|
|
507
|
+
*/
|
|
508
|
+
export function resolveIssueRouting(cwd: string): IssueRouting {
|
|
509
|
+
const empty: IssueRouting = { kind: "keys", backend: null, team: null };
|
|
390
510
|
try {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
511
|
+
const path = configFile(mainCheckoutRoot(cwd));
|
|
512
|
+
if (!existsSync(path)) return empty;
|
|
513
|
+
const text = readFileSync(path, "utf8");
|
|
514
|
+
const issues = parseTomlSubset(text).tables.issues;
|
|
515
|
+
const keys: IssueDestination = {
|
|
516
|
+
backend: strippedString(issues?.backend),
|
|
517
|
+
team: strippedString(issues?.team),
|
|
518
|
+
};
|
|
519
|
+
return issueKeysProvable(text)
|
|
520
|
+
? { kind: "keys", ...keys }
|
|
521
|
+
: { kind: "document", text, ...keys };
|
|
395
522
|
} catch {
|
|
396
|
-
return
|
|
523
|
+
return empty;
|
|
397
524
|
}
|
|
398
525
|
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Read the committed main-checkout `[issues] backend`/`team` values (no validation, no default):
|
|
529
|
+
* the two keys that decide where the Python save lands (`perk/backends/resolve.py`). The
|
|
530
|
+
* `resolveIssueRouting` read without its parity verdict — callers wanting the fail-safe backend
|
|
531
|
+
* id use `resolveIssueBackendId`.
|
|
532
|
+
*/
|
|
533
|
+
export function resolveIssueDestination(cwd: string): IssueDestination {
|
|
534
|
+
const { backend, team } = resolveIssueRouting(cwd);
|
|
535
|
+
return { backend, team };
|
|
536
|
+
}
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
// Node builtins only (so it loads cleanly under `node --test`); shells `git` via `execFileSync`,
|
|
4
4
|
// never with a shell. Fail-open by design: every failure degrades to the caller's `cwd` (or null
|
|
5
5
|
// where stated) rather than throwing — the carriers that use this must never wedge a session.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
6
|
+
// `revalidationBracket` and `worktreeGitDir` deliberately fail closed: snapshot proofs and
|
|
7
|
+
// writer coordination must never invent identity from a failed probe.
|
|
8
8
|
|
|
9
9
|
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { realpathSync, statSync } from "node:fs";
|
|
10
11
|
import { isAbsolute, resolve } from "node:path";
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -35,6 +36,64 @@ export function mainCheckoutRoot(cwd: string): string {
|
|
|
35
36
|
return resolve(common, "..");
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
/** Canonical PER-WORKTREE Git directory for execution exclusion, never a cwd fallback. */
|
|
40
|
+
export function worktreeGitDir(cwd: string): string | null {
|
|
41
|
+
try {
|
|
42
|
+
const out = execFileSync("git", ["rev-parse", "--absolute-git-dir"], {
|
|
43
|
+
cwd,
|
|
44
|
+
encoding: "utf8",
|
|
45
|
+
timeout: 5_000,
|
|
46
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
47
|
+
}).replace(/\r?\n$/, "");
|
|
48
|
+
if (!isAbsolute(out) || /[\0\r\n]/.test(out) || !statSync(out).isDirectory()) return null;
|
|
49
|
+
return realpathSync(out);
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The git-config entries that decide WHERE a GitHub-backed save lands: every `remote.*.url` and
|
|
57
|
+
* `remote.*.gh-resolved` key (`gh` resolves the target repo from exactly these). Returns the
|
|
58
|
+
* NUL-separated `key\nvalue` entries sorted lexicographically and re-joined with `\0` (a
|
|
59
|
+
* canonical form fit for digesting), `""` when the repo has no remotes at all (git exits 1 with
|
|
60
|
+
* empty stdout), or `null` when the destination cannot be verified (not a repo, git missing,
|
|
61
|
+
* any other failure). **Fails closed** on purpose: a caller fencing a save must treat `null` as
|
|
62
|
+
* "unverifiable", never as "unchanged". No other git config participates — `branch.*`,
|
|
63
|
+
* `user.*`, and friends never route a save, so they must never invalidate a review.
|
|
64
|
+
*
|
|
65
|
+
* Repo membership is probed first (`worktreeGitDir`): outside a repo `git config` still reads
|
|
66
|
+
* the global/system files and reports "no match" — which would masquerade as a verified empty
|
|
67
|
+
* destination.
|
|
68
|
+
*/
|
|
69
|
+
export function remoteConfig(cwd: string): string | null {
|
|
70
|
+
if (worktreeGitDir(cwd) === null) return null;
|
|
71
|
+
try {
|
|
72
|
+
const out = execFileSync(
|
|
73
|
+
"git",
|
|
74
|
+
["config", "--null", "--get-regexp", "^remote\\..*\\.(url|gh-resolved)$"],
|
|
75
|
+
{
|
|
76
|
+
cwd,
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
timeout: 5_000,
|
|
79
|
+
maxBuffer: 1024 * 1024,
|
|
80
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
return out
|
|
84
|
+
.split("\0")
|
|
85
|
+
.filter((entry) => entry !== "")
|
|
86
|
+
.sort()
|
|
87
|
+
.join("\0");
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// `git config --get-regexp` exits 1 with empty stdout when NO key matches — a repo with no
|
|
90
|
+
// remotes is a verified (empty) destination, not a failure.
|
|
91
|
+
const failure = error as { status?: unknown; stdout?: unknown };
|
|
92
|
+
if (failure.status === 1 && String(failure.stdout ?? "") === "") return "";
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
38
97
|
/** Run one git command; trimmed stdout, or null on any failure (the module's fail-open style). */
|
|
39
98
|
function git(cwd: string, args: string[], timeout?: number): string | null {
|
|
40
99
|
try {
|
|
@@ -96,6 +155,27 @@ export function headSha(cwd: string): string | null {
|
|
|
96
155
|
return git(cwd, ["rev-parse", "HEAD"]);
|
|
97
156
|
}
|
|
98
157
|
|
|
158
|
+
/** Whether HEAD is a positively-PROVEN unborn branch pointer: `symbolic-ref -q HEAD` resolves
|
|
159
|
+
* AND `for-each-ref` proves the pointed-to ref ABSENT (an exit-0 run, empty output). `false` =
|
|
160
|
+
* the ref EXISTS (a failing `headSha` read was transient, not unborn); **fail-open to null**
|
|
161
|
+
* when either probe fails outright — callers must never read null as unborn. Own `execFileSync`
|
|
162
|
+
* for the second probe: `git()` conflates empty output (absence — meaningful here) with
|
|
163
|
+
* failure. */
|
|
164
|
+
export function unbornHead(cwd: string): boolean | null {
|
|
165
|
+
const pointer = git(cwd, ["symbolic-ref", "-q", "HEAD"]);
|
|
166
|
+
if (pointer === null) return null;
|
|
167
|
+
try {
|
|
168
|
+
const out = execFileSync("git", ["for-each-ref", "--format=%(refname)", pointer], {
|
|
169
|
+
cwd,
|
|
170
|
+
encoding: "utf8",
|
|
171
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
172
|
+
});
|
|
173
|
+
return out.trim() === "";
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
99
179
|
/**
|
|
100
180
|
* Whether the working tree has anything uncommitted (`git status --porcelain`). Untracked files
|
|
101
181
|
* count as dirty — deliberate: the model decides whether they belong in a commit. **Fail-open to
|
|
@@ -122,9 +202,11 @@ export function worktreeDirty(cwd: string): boolean | null {
|
|
|
122
202
|
* `git status --porcelain`, so a status-based cleanliness proof over a flagged index is not a
|
|
123
203
|
* proof. **Fail-open to null** on any failure (not a repo, git missing) — callers must NOT
|
|
124
204
|
* conflate null with "no flags". Own `execFileSync` rather than the `git()` helper: `git()`
|
|
125
|
-
* conflates empty output (an empty index — meaningful here) with failure.
|
|
205
|
+
* conflates empty output (an empty index — meaningful here) with failure. Module-private:
|
|
206
|
+
* `revalidationBracket`'s default flags probe is the one consumer (tests reach the arms through
|
|
207
|
+
* the bracket — real-repo flag arms — and its `probes` seam for the null arm).
|
|
126
208
|
*/
|
|
127
|
-
|
|
209
|
+
function indexHidesChanges(cwd: string): boolean | null {
|
|
128
210
|
try {
|
|
129
211
|
const out = execFileSync("git", ["ls-files", "-v"], {
|
|
130
212
|
cwd,
|
|
@@ -141,8 +223,8 @@ export function indexHidesChanges(cwd: string): boolean | null {
|
|
|
141
223
|
}
|
|
142
224
|
|
|
143
225
|
/**
|
|
144
|
-
* The dream-snapshot revalidation bracket (contracts.md §8.65) —
|
|
145
|
-
* **fail-closed** composition (
|
|
226
|
+
* The dream-snapshot revalidation bracket (contracts.md §8.65) — a deliberately
|
|
227
|
+
* **fail-closed** snapshot composition (an exception to the fail-open probe defaults): it exists
|
|
146
228
|
* to PROVE the repository still matches a stamped snapshot, so an unprovable probe must read as
|
|
147
229
|
* drift, never as "unchanged". The claim is END-STATE equality only — HEAD unchanged, the
|
|
148
230
|
* working tree clean, and no assume-unchanged/skip-worktree index flags (which would hide edits
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Model-visible output capping (route-don't-relay). The byte-cap helper the stage-execution
|
|
2
|
+
// seam and the CI executor bound their model-facing text with: full results stay in scratch
|
|
3
|
+
// files; only a capped, notice-carrying slice reaches the model.
|
|
4
|
+
|
|
5
|
+
/** The model-visible byte cap (matches subagent's PER_TASK_OUTPUT_CAP); overridable per call. */
|
|
6
|
+
export const DEFAULT_MODEL_VISIBLE_CAP = 50 * 1024;
|
|
7
|
+
|
|
8
|
+
export interface CapResult {
|
|
9
|
+
/** The (possibly truncated) text safe to show the model. */
|
|
10
|
+
shown: string;
|
|
11
|
+
/** Total UTF-8 byte length of the original text. */
|
|
12
|
+
bytesTotal: number;
|
|
13
|
+
/** UTF-8 byte length of `shown` (before the truncation notice). */
|
|
14
|
+
bytesShown: number;
|
|
15
|
+
/** Whether truncation occurred. */
|
|
16
|
+
truncated: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* UTF-8-byte-safe truncation (subagent's byte-trim loop). Under cap ⇒ unchanged, truncated:false.
|
|
21
|
+
* When truncated, a notice points at the scratch file holding the full result; the notice sits at
|
|
22
|
+
* the cut edge (appended in head mode, prepended in tail mode) so a top-down reader immediately
|
|
23
|
+
* knows which side is missing.
|
|
24
|
+
*
|
|
25
|
+
* `keep` mirrors the SDK's truncateHead/truncateTail guidance: "head" (default) for
|
|
26
|
+
* model-authored summaries/handoffs where the beginning matters; "tail" for command/CI logs where
|
|
27
|
+
* failure summaries live at the end. Deliberately perk's own byte-only util (not the SDK's
|
|
28
|
+
* line-count-aware `truncateTail`): `CapResult`'s byte fields and the scratch-pointing notice are
|
|
29
|
+
* load-bearing in `CiCheckResult`. Pure.
|
|
30
|
+
*/
|
|
31
|
+
export function capForModel(
|
|
32
|
+
text: string,
|
|
33
|
+
cap: number = DEFAULT_MODEL_VISIBLE_CAP,
|
|
34
|
+
scratchPath: string | null = null,
|
|
35
|
+
keep: "head" | "tail" = "head",
|
|
36
|
+
): CapResult {
|
|
37
|
+
const bytesTotal = Buffer.byteLength(text, "utf8");
|
|
38
|
+
if (bytesTotal <= cap) {
|
|
39
|
+
return { shown: text, bytesTotal, bytesShown: bytesTotal, truncated: false };
|
|
40
|
+
}
|
|
41
|
+
let trimmed = keep === "head" ? text.slice(0, cap) : text.slice(-cap);
|
|
42
|
+
while (Buffer.byteLength(trimmed, "utf8") > cap) {
|
|
43
|
+
trimmed = keep === "head" ? trimmed.slice(0, -1) : trimmed.slice(1);
|
|
44
|
+
}
|
|
45
|
+
const bytesShown = Buffer.byteLength(trimmed, "utf8");
|
|
46
|
+
const omitted = bytesTotal - bytesShown;
|
|
47
|
+
const where = scratchPath ? ` Full output preserved at ${scratchPath}.` : "";
|
|
48
|
+
const shown =
|
|
49
|
+
keep === "head"
|
|
50
|
+
? `${trimmed}\n\n[Output truncated: ${omitted} bytes omitted.${where}]`
|
|
51
|
+
: `[Output truncated: ${omitted} bytes omitted.${where}]\n\n${trimmed}`;
|
|
52
|
+
return { shown, bytesTotal, bytesShown, truncated: true };
|
|
53
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// perk-owned dot-directory path construction — the TS twin of perk/substrate/paths.py
|
|
2
2
|
// (contracts.md §8.1).
|
|
3
3
|
//
|
|
4
|
-
// The sole construction site for the perk-owned config family (`config.toml`/`local.toml`)
|
|
5
|
-
//
|
|
4
|
+
// The sole construction site for the perk-owned config family (`config.toml`/`local.toml`).
|
|
5
|
+
// The config family now lives at `.perk/` (TS reads the target only — the legacy
|
|
6
6
|
// `.pi/perk.toml` migration is Python-side). The workflow family lives in the established cache
|
|
7
7
|
// seam (substrate/cache.ts's `workflowDir`); together these two modules own every perk-owned
|
|
8
8
|
// dot-path on this plane.
|
|
@@ -17,11 +17,6 @@ import { join } from "node:path";
|
|
|
17
17
|
export const CONFIG_FILENAME = "config.toml";
|
|
18
18
|
export const LOCAL_CONFIG_FILENAME = "local.toml";
|
|
19
19
|
|
|
20
|
-
/** The perk-owned dot-dir root (shared with Pi today). */
|
|
21
|
-
export function perkDir(cwd: string): string {
|
|
22
|
-
return join(cwd, ".pi");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
20
|
/** The single config-family redirection point (the file helpers derive from it). */
|
|
26
21
|
export function configDir(cwd: string): string {
|
|
27
22
|
return join(cwd, ".perk");
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
//
|
|
18
18
|
// This seam is LIVE in production: render is imported by the worker, the warm doors, the
|
|
19
19
|
// factories, and the mode/adapter context modules (the `prompts/contexts/` injections).
|
|
20
|
+
//
|
|
21
|
+
// Besides the render seam itself, the module carries ONE cross-plane selector wrapping it:
|
|
22
|
+
// `planReadInstruction`, the per-backend plan-read arm shared by the doors and the
|
|
23
|
+
// stage-execution seam.
|
|
20
24
|
|
|
21
25
|
import { render as miniJinjaRender } from "./miniJinja.ts";
|
|
22
26
|
|
|
@@ -24,3 +28,21 @@ import { render as miniJinjaRender } from "./miniJinja.ts";
|
|
|
24
28
|
export function render(name: string, vars: Record<string, unknown>): string {
|
|
25
29
|
return miniJinjaRender(name, vars);
|
|
26
30
|
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The per-backend plan-read instruction — the prompt SSOT for "how do I read the saved
|
|
34
|
+
* plan". Byte-identical to `src/perk/run/launch/prompts.py::_plan_read_instruction` (the Python
|
|
35
|
+
* twin — likewise a prompts module); drift in either plane fails the paired parity suites.
|
|
36
|
+
* `github` reads via `gh`; `linear` points at the pi-mono-linear tools with an `open <url>`
|
|
37
|
+
* fallback; any other provider falls back to opening the url.
|
|
38
|
+
*
|
|
39
|
+
* The wording lives in the canonical templates `prompts/common/plan-read/*.md`, rendered
|
|
40
|
+
* identically by both planes via the shared render seam (contracts.md §8.31); branching stays in
|
|
41
|
+
* code — only the arm chosen and the vars passed differ. Golden-fixture parity (the three
|
|
42
|
+
* `plan-read-*` cases) plus a thin per-arm selection test replace the dedicated substring parity.
|
|
43
|
+
*/
|
|
44
|
+
export function planReadInstruction(provider: string, prId: string, url: string): string {
|
|
45
|
+
if (provider === "github") return render("common/plan-read/github.md", { pr_id: prId, url });
|
|
46
|
+
if (provider === "linear") return render("common/plan-read/linear.md", { pr_id: prId, url });
|
|
47
|
+
return render("common/plan-read/other.md", { pr_id: prId, url });
|
|
48
|
+
}
|
|
@@ -12,6 +12,8 @@ import { sharedDir } from "./resources.ts";
|
|
|
12
12
|
export interface RegistryStage {
|
|
13
13
|
id: string;
|
|
14
14
|
command: string;
|
|
15
|
+
/** `read-only` | `read-write` — the gate the cold claim persists as `perk:workflow-state.mode`. */
|
|
16
|
+
mode: string;
|
|
15
17
|
doors: Record<string, boolean>;
|
|
16
18
|
predecessors: string[];
|
|
17
19
|
successors: string[];
|