pi-gauntlet 8.3.0 → 8.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +4 -4
- package/bin/gauntlet-performance.mjs +477 -64
- package/bin/gauntlet-performance.test.mjs +27 -27
- package/bin/gauntlet-spec-index.mjs +290 -47
- package/bin/gauntlet-spec-index.test.mjs +42 -18
- package/bin/gauntlet-telemetry-seal.mjs +220 -77
- package/bin/gauntlet-telemetry-seal.test.mjs +222 -457
- package/extensions/lib/gauntlet-settings.test.ts +12 -14
- package/extensions/lib/gauntlet-settings.ts +7 -16
- package/extensions/lib/telemetry-branch.test.ts +102 -0
- package/extensions/lib/telemetry-branch.ts +145 -0
- package/extensions/lib/telemetry-diff.test.ts +2 -2
- package/extensions/lib/telemetry-diff.ts +1 -1
- package/extensions/lib/telemetry-leg.ts +163 -0
- package/extensions/lib/telemetry-record.ts +2 -0
- package/extensions/lib/vcs.ts +6 -23
- package/extensions/phase-tracker.test.ts +6 -3
- package/extensions/phase-tracker.ts +2 -0
- package/extensions/plan-tracker.test.ts +4 -2
- package/extensions/plan-tracker.ts +1 -0
- package/extensions/telemetry-leg-failures.test.ts +138 -0
- package/extensions/telemetry-leg-fields.test.ts +130 -0
- package/extensions/telemetry-leg-no-write.test.ts +90 -0
- package/extensions/telemetry-leg-rounds.test.ts +157 -0
- package/extensions/telemetry-leg-trackers.test.ts +114 -0
- package/extensions/telemetry-leg.test.ts +151 -0
- package/extensions/telemetry-settlement-contract.test.ts +67 -0
- package/extensions/telemetry.test.ts +44 -27
- package/extensions/telemetry.ts +68 -48
- package/extensions/test-support/git-env.mjs +13 -0
- package/package.json +1 -1
- package/skills/brainstorming/SKILL.md +1 -1
- package/skills/check-delivery/SKILL.md +2 -0
- package/skills/finishing-a-development-branch/SKILL.md +13 -13
- package/skills/finishing-a-development-branch/reference/verification-set.md +2 -2
- package/skills/gatekeep-pr/reference/batch.md +3 -1
- package/skills/gatekeep-pr/reference/cleanup.md +1 -1
- package/skills/gatekeep-pr/reference/post-selection-loop.md +7 -1
- package/skills/gauntlet-resume/reference/seed.md +1 -6
- package/skills/shape-ticket/SKILL.md +1 -1
- package/skills/shape-ticket/reference/repo-audit.md +2 -2
package/extensions/telemetry.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telemetry extension: records
|
|
3
|
-
*
|
|
2
|
+
* Telemetry extension: records run and session-leg YAML from pi events alone.
|
|
3
|
+
* Session legs seal through telemetry-leg.ts at terminal settlement. Runs bind
|
|
4
|
+
* only after `phase_tracker start brainstorm` armed the
|
|
4
5
|
* flow, writes the record to disk without ever staging or committing it (the seal bin
|
|
5
6
|
* commits once at finish), and blocks `write` into an already-shipped spec while
|
|
6
7
|
* brainstorm is in progress. Pure helpers: extensions/lib/telemetry-*.ts. The default
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
import * as piRuntime from "@earendil-works/pi-coding-agent";
|
|
15
16
|
import { SettingsManager, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
16
17
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
|
-
import { DEFAULT_TEST_COMMANDS, planDirsFor, resolveFlowGuards, resolveTelemetry, settingsErrorWarning, type TelemetryResolved } from "./lib/gauntlet-settings.ts";
|
|
18
|
+
import { DEFAULT_TELEMETRY_DIR, DEFAULT_TEST_COMMANDS, planDirsFor, resolveFlowGuards, resolveTelemetry, settingsErrorWarning, type TelemetryResolved } from "./lib/gauntlet-settings.ts";
|
|
18
19
|
import { loadGauntletSettings } from "./lib/gauntlet-settings-loader.ts";
|
|
19
20
|
import { checkoutOf } from "./lib/checkout.ts";
|
|
20
21
|
import { CONTEXT_DRAFT_MARKER, nextGauntletEntered } from "./lib/phase-tracker-helpers.ts";
|
|
@@ -24,6 +25,8 @@
|
|
|
24
25
|
import { COUNTED_USER_PHASES, REVIEWER_AGENTS, countFindings, countStructuredFindings, countOpenGaps, countReopens, insertedText, planTotals, textOf, usageToTokens } from "./lib/telemetry-collect.ts";
|
|
25
26
|
import { addTokens, capEvents, compact, currentPhase, derive, diffPhases, emptyAccumulators, emptyPhases, foldAccumulators, implementAutoCompletes, liveShipEvent, newRecord, parseRecord, serializeRecord, type Accumulators, type BaseEvent, type PhaseAcc, type PhaseKey, type PhaseMap, type TelemetryEvent, type TelemetryRecord, type Tokens } from "./lib/telemetry-record.ts";
|
|
26
27
|
import { guardReason } from "./lib/telemetry-ship.ts";
|
|
28
|
+
import { readTelemetryBranch, telemetryRepo, type syncTelemetryBranch } from "./lib/telemetry-branch.ts";
|
|
29
|
+
import { registerLegTelemetry } from "./lib/telemetry-leg.ts";
|
|
27
30
|
import { buildCouncil, parseAudit, parseChairReport, resolveMembers, type Cluster, type MemberResult } from "./lib/telemetry-council.ts";
|
|
28
31
|
|
|
29
32
|
// ---- deps seam -------------------------------------------------------------------
|
|
@@ -51,6 +54,9 @@
|
|
|
51
54
|
testCommands?: string[];
|
|
52
55
|
}
|
|
53
56
|
export interface Deps {
|
|
57
|
+
syncTelemetryBranch?: typeof syncTelemetryBranch;
|
|
58
|
+
telemetryRepo?: typeof telemetryRepo;
|
|
59
|
+
readTelemetryBranch?: typeof readTelemetryBranch;
|
|
54
60
|
fs: FsPort;
|
|
55
61
|
git: (args: string[], cwd: string) => Promise<GitResult>;
|
|
56
62
|
// jj runner; production uses realJj for both checkout detection and the ship-time diff.
|
|
@@ -205,13 +211,16 @@
|
|
|
205
211
|
let boundSpec: string | undefined; // repo-relative
|
|
206
212
|
let record: TelemetryRecord | undefined;
|
|
207
213
|
const predecessorLinks = new Set<string>();
|
|
208
|
-
let recordRel: string | undefined; //
|
|
214
|
+
let recordRel: string | undefined; // absolute record path outside the working tree
|
|
209
215
|
let buffer: TelemetryEvent[] = [];
|
|
210
216
|
let pending: Accumulators = emptyAccumulators();
|
|
211
217
|
let frozen = false;
|
|
218
|
+
let recordWritten = false;
|
|
212
219
|
let gauntletEntered = false;
|
|
213
220
|
let settingsWarned = false;
|
|
214
221
|
let sessionId = "";
|
|
222
|
+
const guardCache = new Map<string, Map<string, string>>();
|
|
223
|
+
let guardWarned = false;
|
|
215
224
|
let currentDir = "";
|
|
216
225
|
let councilPending: { memberResults: MemberResult[]; chair?: { model: string; dispatches: number; clusters?: Cluster[] } } | undefined;
|
|
217
226
|
|
|
@@ -235,7 +244,7 @@
|
|
|
235
244
|
// Disarmed and unbound: activity outside the gauntlet is never charged to the next run.
|
|
236
245
|
const live = (): Accumulators => record
|
|
237
246
|
? (record.accumulators[sessionId] ??= emptyAccumulators())
|
|
238
|
-
: gauntletEntered ? pending : emptyAccumulators();
|
|
247
|
+
: legAcc() ?? (gauntletEntered ? pending : emptyAccumulators());
|
|
239
248
|
|
|
240
249
|
const canBind = (): boolean => gauntletEntered && !frozen && phases.ship.status !== "complete";
|
|
241
250
|
|
|
@@ -252,8 +261,8 @@
|
|
|
252
261
|
const warn = (message: string) => pushEvent({ kind: "warning", message });
|
|
253
262
|
|
|
254
263
|
// ---- record store ----
|
|
255
|
-
// The
|
|
256
|
-
//
|
|
264
|
+
// The seal either keeps a stamped file for retry or removes it after committing.
|
|
265
|
+
// Neither may be overwritten by a later recorder flush.
|
|
257
266
|
const sealedAt = (path: string): boolean => {
|
|
258
267
|
const text = deps.fs.readFile(path);
|
|
259
268
|
const parsed = text ? parseRecord(text) : undefined;
|
|
@@ -266,8 +275,8 @@
|
|
|
266
275
|
frozen = true;
|
|
267
276
|
return;
|
|
268
277
|
}
|
|
269
|
-
const target =
|
|
270
|
-
if (sealedAt(target)) {
|
|
278
|
+
const target = recordRel;
|
|
279
|
+
if ((recordWritten && !deps.fs.exists(target)) || sealedAt(target)) {
|
|
271
280
|
frozen = true;
|
|
272
281
|
return;
|
|
273
282
|
}
|
|
@@ -281,6 +290,7 @@
|
|
|
281
290
|
deps.fs.mkdirp(dirname(target));
|
|
282
291
|
deps.fs.writeFile(target + ".tmp", serializeRecord(record));
|
|
283
292
|
deps.fs.rename(target + ".tmp", target);
|
|
293
|
+
recordWritten = true;
|
|
284
294
|
} catch (e) {
|
|
285
295
|
pushEvent({ kind: "warning", message: `record write failed: ${String((e as Error).message ?? e).split("\n")[0]}` });
|
|
286
296
|
}
|
|
@@ -288,27 +298,9 @@
|
|
|
288
298
|
|
|
289
299
|
const flush = writeRecord;
|
|
290
300
|
|
|
291
|
-
// Excluded untracked paths are invisible to `git status --porcelain`, skipped by
|
|
292
|
-
// `git add -A`, and never block `git worktree remove`; the seal stages with `git add -f`.
|
|
293
|
-
const ensureExcluded = async () => {
|
|
294
|
-
const common = await deps.git(["rev-parse", "--path-format=absolute", "--git-common-dir"], toplevel!);
|
|
295
|
-
const commonDir = common.stdout.trim();
|
|
296
|
-
if (common.code !== 0 || !commonDir) return void warn(`exclude skipped: git rev-parse --git-common-dir failed: ${common.stderr.trim().split("\n")[0]}`);
|
|
297
|
-
const line = `${currentDir.replace(/\/+$/, "")}/`;
|
|
298
|
-
const file = join(commonDir, "info", "exclude");
|
|
299
|
-
const existing = deps.fs.readFile(file) ?? "";
|
|
300
|
-
if (existing.split("\n").some((l) => l.trim() === line)) return;
|
|
301
|
-
try {
|
|
302
|
-
deps.fs.mkdirp(dirname(file));
|
|
303
|
-
deps.fs.writeFile(file, existing + (existing && !existing.endsWith("\n") ? "\n" : "") + line + "\n");
|
|
304
|
-
} catch (e) {
|
|
305
|
-
warn(`exclude write failed: ${String((e as Error).message ?? e).split("\n")[0]}`);
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
|
|
309
301
|
const loadOrCreate = (specRel: string): TelemetryRecord => {
|
|
310
302
|
const rel = recordPathFor(currentDir, specRel);
|
|
311
|
-
const existing = deps.fs.readFile(
|
|
303
|
+
const existing = deps.fs.readFile(rel);
|
|
312
304
|
const parsed = existing ? parseRecord(existing) : undefined;
|
|
313
305
|
if (parsed) {
|
|
314
306
|
// fold stale live blocks (sessions that died without shutdown) into total
|
|
@@ -324,13 +316,40 @@
|
|
|
324
316
|
return newRecord({ spec: specRel, session: sessionId, now: buffer[0]?.ts ?? deps.now(), runId: randomUUID() });
|
|
325
317
|
};
|
|
326
318
|
|
|
327
|
-
const
|
|
319
|
+
const shippedRecords = (top: string, snap: SettingsSnapshot): Map<string, string> => {
|
|
320
|
+
const key = JSON.stringify([top, snap.telemetry.remote]);
|
|
321
|
+
let records = guardCache.get(key);
|
|
322
|
+
if (!records) {
|
|
323
|
+
records = new Map();
|
|
324
|
+
guardCache.set(key, records);
|
|
325
|
+
const repo = (deps.telemetryRepo ?? telemetryRepo)(top);
|
|
326
|
+
const result = (deps.readTelemetryBranch ?? readTelemetryBranch)(top, { remote: snap.telemetry.remote, prefix: `${repo}/brainstorming/` });
|
|
327
|
+
for (const entry of result.records) {
|
|
328
|
+
const parsed = parseRecord(entry.bytes);
|
|
329
|
+
if (parsed?.shipped_at) records.set(parsed.spec, parsed.shipped_at);
|
|
330
|
+
}
|
|
331
|
+
if (result.warning && !guardWarned) {
|
|
332
|
+
guardWarned = true;
|
|
333
|
+
pi.sendMessage({ customType: "telemetry-warning", content: `telemetry guard unavailable: ${result.warning}`, display: true });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return records;
|
|
337
|
+
};
|
|
328
338
|
|
|
329
339
|
const bind = async (specRel: string, snap: SettingsSnapshot, specToplevel: string, via: "git" | "jj") => {
|
|
330
|
-
if (
|
|
340
|
+
if (via === "git" && shippedRecords(specToplevel, snap).has(specRel)) return;
|
|
341
|
+
let metadataDir = join(specToplevel, ".jj");
|
|
342
|
+
if (via === "git") {
|
|
343
|
+
const common = await deps.git(["rev-parse", "--path-format=absolute", "--git-common-dir"], specToplevel);
|
|
344
|
+
if (common.code !== 0 || !common.stdout.trim()) {
|
|
345
|
+
warn(`record bind skipped: git common dir unavailable: ${common.stderr.trim().split("\n")[0]}`);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
metadataDir = common.stdout.trim();
|
|
349
|
+
}
|
|
331
350
|
toplevel = specToplevel;
|
|
332
351
|
checkoutVia = via;
|
|
333
|
-
currentDir =
|
|
352
|
+
currentDir = join(metadataDir, "gauntlet", "telemetry");
|
|
334
353
|
boundSpec = specRel;
|
|
335
354
|
recordRel = recordPathFor(currentDir, specRel);
|
|
336
355
|
predecessorLinks.clear();
|
|
@@ -363,7 +382,6 @@
|
|
|
363
382
|
if (snap.errors.length) warn(settingsErrorWarning(snap.errors));
|
|
364
383
|
if (snap.telemetry.warning) warn(snap.telemetry.warning);
|
|
365
384
|
}
|
|
366
|
-
if (via === "git") await ensureExcluded();
|
|
367
385
|
refreshLinks();
|
|
368
386
|
};
|
|
369
387
|
|
|
@@ -374,6 +392,7 @@
|
|
|
374
392
|
record = undefined;
|
|
375
393
|
recordRel = undefined;
|
|
376
394
|
frozen = false;
|
|
395
|
+
recordWritten = false;
|
|
377
396
|
councilPending = undefined;
|
|
378
397
|
};
|
|
379
398
|
|
|
@@ -400,18 +419,13 @@
|
|
|
400
419
|
const rebind = async (newSpec: string): Promise<boolean> => {
|
|
401
420
|
const from = recordRel!;
|
|
402
421
|
const to = recordPathFor(currentDir, newSpec);
|
|
403
|
-
if (deps.fs.exists(
|
|
422
|
+
if (deps.fs.exists(to)) {
|
|
404
423
|
warn(`record move skipped: ${to} already exists`);
|
|
405
424
|
return false;
|
|
406
425
|
}
|
|
407
|
-
const trackedResult = await deps.git(["ls-files", "--error-unmatch", "--", from], toplevel!);
|
|
408
|
-
const tracked = trackedResult.code === 0 && trackedResult.stdout.trim() !== "";
|
|
409
426
|
try {
|
|
410
|
-
deps.fs.mkdirp(dirname(
|
|
411
|
-
|
|
412
|
-
const mv = await deps.git(["mv", "--", from, to], toplevel!);
|
|
413
|
-
if (mv.code !== 0) deps.fs.rename(abs(from), abs(to));
|
|
414
|
-
} else deps.fs.rename(abs(from), abs(to));
|
|
427
|
+
deps.fs.mkdirp(dirname(to));
|
|
428
|
+
deps.fs.rename(from, to);
|
|
415
429
|
} catch (e) {
|
|
416
430
|
warn(`record move failed: ${String((e as Error).message ?? e).split("\n")[0]}`);
|
|
417
431
|
return false;
|
|
@@ -493,8 +507,11 @@
|
|
|
493
507
|
lastPlanTasks = undefined;
|
|
494
508
|
amendmentOpen = false;
|
|
495
509
|
pendingTest.clear();
|
|
510
|
+
if (sessionId !== ctx.sessionManager.getSessionId()) {
|
|
511
|
+
guardCache.clear();
|
|
512
|
+
guardWarned = false;
|
|
513
|
+
}
|
|
496
514
|
sessionId = ctx.sessionManager.getSessionId();
|
|
497
|
-
currentDir = snap.telemetry.dir;
|
|
498
515
|
const replay = replayBranch(ctx.sessionManager.getBranch(), snap.specDirs);
|
|
499
516
|
lastPlanTasks = replay.lastPlanTasks;
|
|
500
517
|
phases = replay.phases;
|
|
@@ -535,6 +552,10 @@
|
|
|
535
552
|
await applyPhaseDetails(event.details as never, snap);
|
|
536
553
|
return undefined;
|
|
537
554
|
}
|
|
555
|
+
if (event.toolName === "subagent" && legAcc()) {
|
|
556
|
+
await onSubagentResult(event.details, event.content, event.isError);
|
|
557
|
+
return undefined;
|
|
558
|
+
}
|
|
538
559
|
if (!record && !canBind()) return undefined;
|
|
539
560
|
if (event.toolName === "plan_check" && !event.isError) {
|
|
540
561
|
const d = event.details as { status?: string; specPath?: string; planPath?: string } | undefined;
|
|
@@ -605,7 +626,7 @@
|
|
|
605
626
|
}
|
|
606
627
|
if (loc.toplevel !== toplevel) {
|
|
607
628
|
// Another checkout: a fresh run, never a rename of this record.
|
|
608
|
-
if ((tool === "write" || tool === "edit") && isSpecPath(rel, snap.specDirs) && canBind() && !
|
|
629
|
+
if ((tool === "write" || tool === "edit") && isSpecPath(rel, snap.specDirs) && canBind() && (loc.via === "jj" || !shippedRecords(loc.toplevel, snap).has(rel))) {
|
|
609
630
|
unbind();
|
|
610
631
|
await bind(rel, snap, loc.toplevel, loc.via);
|
|
611
632
|
if (!boundSpec) return undefined;
|
|
@@ -751,7 +772,7 @@
|
|
|
751
772
|
if (!record || !toplevel || liveShipEvent(record.events)) return;
|
|
752
773
|
record.status = "shipped";
|
|
753
774
|
record.shipped_at = deps.now();
|
|
754
|
-
const out = await computeJjDiff({ jj: deps.jj ?? realJj, cwd: toplevel, spec: record.spec, dir:
|
|
775
|
+
const out = await computeJjDiff({ jj: deps.jj ?? realJj, cwd: toplevel, spec: record.spec, dir: DEFAULT_TELEMETRY_DIR, buckets: snap.telemetry.buckets, planDirs: snap.planDirs });
|
|
755
776
|
if (out.warning) warn(out.warning);
|
|
756
777
|
record.derived.modified_files = out.modified_files;
|
|
757
778
|
record.derived.diff = out.diff;
|
|
@@ -766,11 +787,9 @@
|
|
|
766
787
|
const rawPath = (event.input as { path?: unknown }).path;
|
|
767
788
|
if (typeof rawPath === "string" && phases.brainstorm.status === "in_progress") {
|
|
768
789
|
const loc = await locate(ctx, rawPath);
|
|
769
|
-
if (loc && isSpecPath(loc.rel, snap.specDirs)) {
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
const parsed = existing ? parseRecord(existing) : undefined;
|
|
773
|
-
if (parsed?.shipped_at) return { block: true, reason: guardReason(loc.rel, parsed.shipped_at, recRel) };
|
|
790
|
+
if (loc?.via === "git" && isSpecPath(loc.rel, snap.specDirs)) {
|
|
791
|
+
const shippedAt = shippedRecords(loc.toplevel, snap).get(loc.rel);
|
|
792
|
+
if (shippedAt) return { block: true, reason: guardReason(loc.rel, shippedAt, "gauntlet/telemetry") };
|
|
774
793
|
}
|
|
775
794
|
}
|
|
776
795
|
return undefined;
|
|
@@ -874,4 +893,5 @@
|
|
|
874
893
|
refreshLinks();
|
|
875
894
|
};
|
|
876
895
|
|
|
896
|
+
const legAcc = registerLegTelemetry(pi, deps, () => !!record);
|
|
877
897
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function isolatedGitEnv(scratch) {
|
|
5
|
+
const env = {
|
|
6
|
+
GIT_CONFIG_GLOBAL: join(scratch, "gitconfig"),
|
|
7
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
8
|
+
GIT_AUTHOR_NAME: "test", GIT_AUTHOR_EMAIL: "test@example.test",
|
|
9
|
+
GIT_COMMITTER_NAME: "test", GIT_COMMITTER_EMAIL: "test@example.test",
|
|
10
|
+
};
|
|
11
|
+
writeFileSync(env.GIT_CONFIG_GLOBAL, "");
|
|
12
|
+
return env;
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -69,7 +69,7 @@ The spec is the first commit in a dedicated worktree, never a separate commit on
|
|
|
69
69
|
|
|
70
70
|
Read [reference/legs.md](reference/legs.md) and settle membership before creation. Invoke `/skill:using-git-worktrees` for each member with explicit `$ROOT` = its primary and `$BRANCH` = the shared branch; carry every `Worktree ready at <full-path>` value, home first. Root the home spec at `<home worktree>/<home spec dir>/<filename>.md`; root each dispatch and `git -C` in its owning leg's worktree. Keep the process cwd unchanged. Gather each leg's rules and record portable contracts and companion artifacts using that reference; a one-member run adds only its one-block `## Legs` section.
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
Keep spec, plan, and implementation in this worktree. Ship the spec and implementation; strip only the plan. Let `/skill:finishing-a-development-branch` seal and publish the telemetry record to the orphan `gauntlet/telemetry` branch before landing; keep recorder state in Git common-dir metadata, outside the working tree. In a plain jj workspace, retain local metadata and report that publication is unavailable. Use no worktree for explicit trivial one-off edits outside this flow.
|
|
73
73
|
|
|
74
74
|
## The Process
|
|
75
75
|
|
|
@@ -262,6 +262,8 @@ Start the message with `gauntlet: gate=check-delivery/confirmation-gate` before
|
|
|
262
262
|
|
|
263
263
|
Start the message with `gauntlet: done delivered` when the batched write landed. Start the message with `gauntlet: done already-recorded` for idempotent delivery. Start the message with `gauntlet: done manual` when the write halves are emitted for manual execution. Start the message with `gauntlet: stuck` followed on that line by the concrete reason once the findings-comment gate is answered or no comment is offered; use this for any partial write failure too. A declined success write ends with `gauntlet: stuck write declined`.
|
|
264
264
|
|
|
265
|
+
After every selected write or follow-on step finishes, put one plain `<ref>: <outcome>` line below the terminal marker, including singleton and early STOP paths. Use the supplied ticket ref and exactly one outcome: `delivered`, `manual`, `declined`, `already recorded`, `skipped`, or `failed - <reason>`. Map a declined success write to `declined`, skipped-no-tracker to `skipped`, and a failed stage, findings-only path or partial write to `failed - <reason>`; report a terminal ticket without recorded delivery as `skipped`. Keep rows outside tracker payloads, without bullets, code fences or emphasis. In set mode use `reference/batch.md`'s one final roster settlement, never a per-ticket terminal marker while siblings continue.
|
|
266
|
+
|
|
265
267
|
Any `unexplained gap` or failed stage -> **failure path**: no status
|
|
266
268
|
advance, no delivery comment, no marker. The findings may be offered as a
|
|
267
269
|
**findings comment** (no `Delivered:` line, so it never trips idempotency) -
|
|
@@ -226,7 +226,7 @@ Rows 1-2 run the Option 1/2 blocks; row 3 runs the Keep block and row 4 the Disc
|
|
|
226
226
|
|
|
227
227
|
#### Strip the plan, seal the record (Options 1-3)
|
|
228
228
|
|
|
229
|
-
Run this on the feature branch before any landing path.
|
|
229
|
+
Run this on the feature branch before any landing path. Ship the spec and implementation; strip only the plan. Let the seal publish the telemetry record to the orphan `gauntlet/telemetry` branch without changing the product checkout, index or HEAD. Keep in-progress records in Git common-dir metadata, outside the working tree. Resolve `<bin>` as `<directory of this skill's SKILL.md>/../../bin` from the skill's `<location>` in the system prompt.
|
|
230
230
|
|
|
231
231
|
Read the existing artifact's path-only `**Spec:**` header (repo-relative, backticks stripped) before removal; pass that value as `--spec`, including for a line-1 ledger. Resolve the spec dirs with `gauntlet_setting({ key: "flowGuards" })` and use their sibling `plans` dirs for plan paths. Enumerate both PLAN_PATH candidates: the task plan paired by basename and `<sibling plans dir>/<spec stem>-ledger.md`. Preserve the spec and telemetry.
|
|
232
232
|
|
|
@@ -242,13 +242,13 @@ elif [ -f "$WORKTREE/$PLAN_PATH" ] && jj -R "$WORKTREE" root >/dev/null 2>&1; th
|
|
|
242
242
|
fi
|
|
243
243
|
done
|
|
244
244
|
|
|
245
|
-
# Seal the telemetry record: stamp shipped, compute the diff,
|
|
246
|
-
# The bin
|
|
245
|
+
# Seal the telemetry record: stamp shipped, compute the diff, publish to gauntlet/telemetry.
|
|
246
|
+
# The bin reports publication unavailable in a plain jj workspace and keeps local metadata.
|
|
247
247
|
# --option is pr for Options 1-2 and squash for Option 3; --spec is the artifact header's **Spec:** value (omit when no artifact is known).
|
|
248
248
|
node <bin>/gauntlet-telemetry-seal.mjs --worktree "$WORKTREE" --option <pr|squash> --base <base-branch> --spec <spec path>
|
|
249
249
|
```
|
|
250
250
|
|
|
251
|
-
|
|
251
|
+
Print the seal's stdout verbatim in the ship completion message. Continue landing on exit 0 for every telemetry outcome, including `no telemetry run`, `telemetry: skipped (unparseable record <path>)` and `telemetry: not pushed (<reason>)`; missing or malformed records and publication failures never block shipping. Retain an unparseable record in place and preserve unpublished telemetry for retry. In a plain jj workspace, report `telemetry: not pushed (jj workspace: no git remote)` and retain local metadata. Stop before the first landing command only on exit 1 for a usage error (invalid arguments or base), quote the bin's line and ask the user how to proceed.
|
|
252
252
|
#### PR-subject row 1: Push to the PR branch
|
|
253
253
|
For the close-absorbed-PR question, start the message with `gauntlet: gate=finishing-a-development-branch/pr-subject-row-1`. Read and apply [menu-layout](../brainstorming/reference/menu-layout.md). Do not report `done pushed` while this gate awaits an answer. Capture the scoped verification set and accepted-complete task/thread evidence before the strip. Refetch threads per recorded PR using the paginated single-`$n` query (one invocation per PR) owned by [pr-head](../using-git-worktrees/reference/pr-head.md); retain PR attribution and use the query's location rendering. For home, use the table below and omit threads now resolved on GitHub. For members, copy recorded dispositions and mark newly resolved or newly opened states from the refresh; never use home's first-parent history as member-fix proof.
|
|
254
254
|
| Refetched thread / evidence | Resolution |
|
|
@@ -346,7 +346,7 @@ git -C "$PRIMARY" commit -m "<imperative summary> (ref <ticket-id>)"
|
|
|
346
346
|
(cd "$PRIMARY" && <the Step 1 set>)
|
|
347
347
|
```
|
|
348
348
|
|
|
349
|
-
|
|
349
|
+
Keep only the spec and implementation in the staged squash tree; the stripped plan stays in feature history and the sealed telemetry record belongs to `gauntlet/telemetry`.
|
|
350
350
|
|
|
351
351
|
The post-squash re-verify is not optional - `git merge --squash` can surface conflict-resolution mistakes the worktree-side run couldn't catch. For a ledger, run the per-pair post-squash recipe in [verification-set.md](reference/verification-set.md) instead of the non-ledger subshell above.
|
|
352
352
|
|
|
@@ -356,7 +356,7 @@ Cleanup worktree (Step 6), then, if Step 6 removed the worktree, `git -C "$PRIMA
|
|
|
356
356
|
|
|
357
357
|
#### Option 4: Keep As-Is
|
|
358
358
|
|
|
359
|
-
Report: "Keeping branch <name>. Worktree preserved at <path>."
|
|
359
|
+
Report: "Keeping branch <name>. Worktree preserved at <path>." Leave telemetry in local metadata; Keep As-Is publishes no record.
|
|
360
360
|
|
|
361
361
|
**Don't cleanup worktree.**
|
|
362
362
|
|
|
@@ -374,7 +374,7 @@ Type 'discard' to confirm.
|
|
|
374
374
|
|
|
375
375
|
Wait for exact confirmation.
|
|
376
376
|
|
|
377
|
-
If confirmed: Cleanup worktree (Step 6), then, if Step 6 removed the worktree, `git -C "$PRIMARY" branch -D "$FEATURE"`.
|
|
377
|
+
If confirmed: Cleanup worktree (Step 6), then, if Step 6 removed the worktree, `git -C "$PRIMARY" branch -D "$FEATURE"`. Publish no telemetry for Discard; leave common-dir metadata outside worktree cleanup.
|
|
378
378
|
|
|
379
379
|
### Step 6: Cleanup Workspace
|
|
380
380
|
|
|
@@ -449,12 +449,12 @@ A host-owned worktree (Step 6: no `home:` match, or the legacy "Otherwise") keep
|
|
|
449
449
|
- **Fix:** Require typed "discard" confirmation
|
|
450
450
|
|
|
451
451
|
**Skipping the strip-and-seal block in Options 1-3 (any path that lands on base)**
|
|
452
|
-
- **Problem:**
|
|
453
|
-
- **Fix:** Run the block on `$WORKTREE` before the squash or
|
|
452
|
+
- **Problem:** Skipping the block ships an ephemeral plan or skips publication of the record the spec index reads.
|
|
453
|
+
- **Fix:** Run the block on `$WORKTREE` before the squash or push. Preserve the spec on `<base-branch>` and the telemetry record on `gauntlet/telemetry`; retain the plan only in feature history.
|
|
454
454
|
|
|
455
|
-
**
|
|
456
|
-
- **Problem:**
|
|
457
|
-
- **Fix:**
|
|
455
|
+
**Treating telemetry as a checkout deliverable**
|
|
456
|
+
- **Problem:** Staging a recorder file pollutes the product diff and bypasses branch publication.
|
|
457
|
+
- **Fix:** Let the seal own publication from common-dir metadata; stage no telemetry record in the product checkout.
|
|
458
458
|
|
|
459
459
|
## Completion
|
|
460
460
|
Start the final outcome message with exactly one terminal line before summaries. Start the message with `gauntlet: done pr-created` after successful normal or draft PR creation. Start the message with `gauntlet: done merged` after successful squash-merge and post-merge verification. Start the message with `gauntlet: done kept` after Keep. Start the message with `gauntlet: done discarded` only after typed discard authorization and successful discard. Start the message with `gauntlet: done pushed` after successful PR-subject push and body edit, only after the close-member gate is answered or no members qualify. Start the message with `gauntlet: stuck` followed on that line by a short concrete reason for an executed operation that fails, before error evidence or a return menu. Do not combine a terminal marker with a gate marker in one message; a subsequent menu starts a new gate message. Pending choices, implementation stalls and conformance escalation are not terminal outcomes. Read and apply [menu-layout](../brainstorming/reference/menu-layout.md). Preserve strip, seal, cleanup and ship-state rules below; terminal reporting does not complete ship after Discard or failure. Once the chosen option (Options 1-4 or PR-subject row 1 - not Discard) is executed successfully, mark the ship phase complete:
|
|
@@ -478,7 +478,7 @@ Once the merge (and any deploy) has landed, `/skill:check-delivery <ticket-ref>`
|
|
|
478
478
|
- Run any step without the `<worktree-path>` argument
|
|
479
479
|
- Auto-proceed past an undispositioned carried-open gap
|
|
480
480
|
- Skip the strip-and-seal block before the Option 3 squash or the Option 1/2 push
|
|
481
|
-
- Delete
|
|
481
|
+
- Delete common-dir telemetry metadata or stage it into the product checkout
|
|
482
482
|
|
|
483
483
|
**Always:**
|
|
484
484
|
- Verify, or apply the Step 1 skip rule, before offering options
|
|
@@ -19,7 +19,7 @@ For ledger commands outside a checkout's configured test-command matcher, verify
|
|
|
19
19
|
|
|
20
20
|
## Skip proof
|
|
21
21
|
|
|
22
|
-
**Skip rule.** Skip the run only when this set passed on a known clean commit in the verify phase of this session and no edit or write landed
|
|
22
|
+
**Skip rule.** Skip the run only when this set passed on a known clean commit in the verify phase of this session and no checkout edit or write landed since. Check the entire current tree: `git -C "$WORKTREE" diff --quiet <commit the run passed on> -- .` must exit 0, and `git -C "$WORKTREE" status --porcelain --untracked-files=all -- .` must exit 0 with empty output. Cover committed, staged, unstaged, and untracked changes without path exclusions; recorder metadata lives outside the working tree. Otherwise run it once; a missing verified commit, a failed check, or uncertainty means run.
|
|
23
23
|
|
|
24
24
|
Apply the proof independently in each owning checkout. Post-squash verification is mandatory; the skip proof does not cover a changed primary tree.
|
|
25
25
|
|
|
@@ -31,4 +31,4 @@ For a fresh-session retry after a ledger was stripped, recover the committed pre
|
|
|
31
31
|
|
|
32
32
|
## Post-squash execution
|
|
33
33
|
|
|
34
|
-
For Option 3, run each delivered leg's retained pairs in order as `(cd "<leg primary>" && <command>) > "$LOG_DIR/gate-<i>.log" 2>&1`, one command per pair
|
|
34
|
+
For Option 3, run each delivered leg's retained pairs in order as `(cd "<leg primary>" && <command>) > "$LOG_DIR/gate-<i>.log" 2>&1`, one command per pair; print `gate <i> exit <code> <log path>` after each and stop on the first failure. For coordinated delivery, run only the just-squashed leg's pairs at that leg's delivery step; every remaining leg gets its own post-squash run when delivered. A failed-squash receipt retry runs those same pairs against the delivered primary, not the feature worktree, and never repeats the squash. Retain the Step 1 command set and execution form for non-ledger subjects.
|
|
@@ -92,6 +92,8 @@ Show evidence only in-place from record/helper paths, then re-ask. Queue all oth
|
|
|
92
92
|
|
|
93
93
|
Run parent cleanup/stop only after all children finish, never on a live child's tree. For cleanup apply cleanup guards and ordered overrides. Keep the PR and post rows in the roster, set cleanup_done, and re-render with cleanup gone. Cleanup does not retire.
|
|
94
94
|
|
|
95
|
-
For non-final stop run `cleanup.md` `## Non-final stop` after all children finish. Under terminal overlays leave checkout as found and print leftover path/branch.
|
|
95
|
+
For non-final stop run `cleanup.md` `## Non-final stop` after all children finish. Under terminal overlays leave checkout as found and print leftover path/branch. Retain the PR's row from `post-selection-loop.md` `### Settlement rows` in the parent roster before deleting only that PR's record, helper outputs, and log_path files under existing teardown rules, then retire it. While any PR remains, start a retirement message with `gauntlet: retired #<N> - <reason>`, never `gauntlet: done` or `gauntlet: stuck`; keep private child result lines unchanged. Await all next-batch results and re-render every nonretired PR; repeat until all retire.
|
|
96
|
+
|
|
97
|
+
After the last PR retires and every selected cleanup or follow-on step finishes, render one final message. Start the message with `gauntlet: stuck retired` when any retained outcome is `stuck - <reason>`, otherwise `gauntlet: done retired`. Print every retained PR row below it in roster order, including PRs retired in earlier rounds. Emit no per-PR terminal message before this set settlement.
|
|
96
98
|
|
|
97
99
|
Initialize parent tracker tasks `pr-<N> state: <record path>`; mark in_progress on dispatch, complete on retirement, failed on unretried failure. Child trackers are session-local stages, not parent roster tasks. Use a plain checklist if plan_tracker is unavailable.
|
|
@@ -22,7 +22,7 @@ Consequence clause for the menu row (`decision-menu.md`): `remove <worktree.path
|
|
|
22
22
|
|
|
23
23
|
Follow the [shared layout](../../brainstorming/reference/menu-layout.md).
|
|
24
24
|
|
|
25
|
-
Start the message with `gauntlet: stuck` followed on that line by the reason: use the obstacle's `stuck - ` text as the reason, or `stopped at <template id>` for initial, assess-terminal, and contract-exception. On a failed reset/assertion use the actual error instead.
|
|
25
|
+
Finish the stop consequences below before the final message. Start the message with `gauntlet: stuck` followed on that line by the reason: use the obstacle's `stuck - ` text as the reason, or `stopped at <template id>` for initial, assess-terminal, and contract-exception. On a failed reset/assertion use the actual error instead. Print the PR row from `post-selection-loop.md` `### Settlement rows` with `stuck - <reason>` below the marker, then cleanup results. In set mode use `batch.md`'s per-PR retirement marker and retain the row for the final roster settlement.
|
|
26
26
|
|
|
27
27
|
Enter here for `stop` on the initial, assess-terminal, obstacle, or contract-exception menu. On a `rebased locally` head, run `git -C <worktree.path> reset --hard <pre_head>` first; assert `HEAD == <pre_head>` and a clean tree. Stop on a failed assertion.
|
|
28
28
|
|
|
@@ -195,9 +195,15 @@ On completion or timeout, refresh then run steps 2-5 of Re-render plus step 1's
|
|
|
195
195
|
|
|
196
196
|
Before posting or committing any external payload - review bodies, replies, commit subjects, tracker comments - re-read it against SKILL.md `## Wording rules` and any `## comms style` rules: ASCII only; no headings or template scaffolding under ~150 words; findings `file:line`-specific where one exists; ends on the fix or asked action, not a recap; never invents content to fill a section. The `AC coverage` block is the one exception: it posts verbatim, heading and checkbox rows included.
|
|
197
197
|
|
|
198
|
+
### Settlement rows
|
|
199
|
+
|
|
200
|
+
Emit the terminal message only after every selected follow-on step finishes and the cleanup pick is answered (run or declined by stop); a report or menu is not settlement. Below its marker print one plain `#<N>@<head sha>: <outcome>[ (<ticket ref>)]` row per PR in roster order, singleton included. Use the full assessed `headRefOid`, not the merge commit or a shortened display SHA. Use the last gathered head when assessment could not start. Append the parenthetical only when the PR title or body's first line names a ticket under the overrides `## Issue tracker` `refs:` convention; otherwise omit it. Use exactly `approved`, `merged`, `closed-pr`, `final`, `merge-queued`, or `stuck - <reason>`, retaining the actual stop/error reason. Keep rows without bullets, code fences or emphasis; put checkout paths and cleanup results after them.
|
|
201
|
+
|
|
202
|
+
For set mode, retain each row before deleting its PR record and follow `batch.md`'s retirement and final-settlement rules. Keep private child result lines unchanged.
|
|
203
|
+
|
|
198
204
|
### Teardown
|
|
199
205
|
|
|
200
|
-
Start the message with `gauntlet: done approved` for stop on approved. Start the message with `gauntlet: done merged` for stop on own-merge. Start the message with `gauntlet: done closed-pr` for stop on closed-pr. Start the message with `gauntlet: done final` for stop on final. Start the message with `gauntlet: done merge-queued` for stop on merge-queued.
|
|
206
|
+
Start the message with `gauntlet: done approved` for stop on approved. Start the message with `gauntlet: done merged` for stop on own-merge. Start the message with `gauntlet: done closed-pr` for stop on closed-pr. Start the message with `gauntlet: done final` for stop on final. Start the message with `gauntlet: done merge-queued` for stop on merge-queued. Print the row from `### Settlement rows`, then the checkout path and local branch. A report Verdict above a menu is not a terminal. Start the message with `gauntlet: stuck` followed by the actual error on any hard assertion failure, including a delegated fix-wave restore/assert failure; print the PR row with `stuck - <actual error>` below it and end with the actual error, without a continuation menu. In set mode use `batch.md`'s per-PR retirement marker until every PR retires.
|
|
201
207
|
|
|
202
208
|
Delegate non-final stop (initial, assess terminal, obstacle, contract exception) to `cleanup.md` `## Non-final stop`. Keep terminal cleanup behind its own pick; terminal stop leaves the checkout as found and reports its path and local branch. Render cleanup only on terminal templates; never run cleanup steps 2-4 for a non-final stop.
|
|
203
209
|
|
|
@@ -13,12 +13,7 @@ Before starting the seed sequence, read the seed's header. A `**Home:**` header
|
|
|
13
13
|
stop: `not tracked: <seed> - commit it first`. Line 1 of `<seed>` equal to
|
|
14
14
|
`# CONTEXT DRAFT - NOT A SPEC - fully replaced at spec-writing` -> stop:
|
|
15
15
|
`context draft, not a spec: <seed> - finish brainstorming`.
|
|
16
|
-
3. **Not shipped.**
|
|
17
|
-
(`piGauntlet.telemetry.dir`, default `.pi/gauntlet/telemetry`). A `shipped_at:` or
|
|
18
|
-
`abandoned_at:` line carrying a value -> stop:
|
|
19
|
-
`spec already shipped at <ts> - write a superseding spec`. No record, or
|
|
20
|
-
`in_progress`: continue; the extension folds this run into the record on
|
|
21
|
-
`plan_check` pass.
|
|
16
|
+
3. **Not shipped.** Resolve `piGauntlet.telemetry` from preset and primary project settings; `enabled: false` skips this check. Run `git -C <primary> rev-parse --git-common-dir`; resolve a relative result against `<primary>` and read `<git common dir>/gauntlet/telemetry/<rel with .md -> .yaml>`. When git resolution fails and `jj root` in `<primary>` succeeds, read `<jj root>/.jj/gauntlet/telemetry/<rel with .md -> .yaml>` instead. When the local record is absent, fetch `gauntlet/telemetry` from `telemetry.remote` (default `origin`) and read `<owner>/<repo>/brainstorming/<rel with .md -> .yaml>` from the fetched ref, using the last fetched ref on fetch failure. Degrade to the local-only check when no fetched ref is available, including jj-only workspaces. A valued `shipped_at:` or `abandoned_at:` stops: `spec already shipped at <ts> - write a superseding spec`. No record or `in_progress`: continue; let the extension bind on `plan_check` pass.
|
|
22
17
|
4. **Name.** `<name>` = basename of `<rel>` minus `.md`, minus a leading `YYYY-MM-DD-`:
|
|
23
18
|
`2026-09-17-gh-31-gauntlet-resume.md` -> `gh-31-gauntlet-resume`. Same shape as
|
|
24
19
|
brainstorming's Filename Convention slug; wrappers receive it as `<name>`.
|
|
@@ -112,7 +112,7 @@ See Roast below. Surface source-gap findings at step 8; never apply them by inve
|
|
|
112
112
|
|
|
113
113
|
### 8 - Confirmation gate
|
|
114
114
|
|
|
115
|
-
Finish initial preparation and roasts for all eligible tickets before the first gate. Present only the current ticket in ref order: decline skips it; revision rechecks and re-presents its exact diff; approval completes step 9 before the next ticket prints. Never treat a reply as approval of an unseen ticket. Present stopped tickets' draft/report at their turn with no approval option; preserve a split's N-body subset-selection gate. End with
|
|
115
|
+
Finish initial preparation and roasts for all eligible tickets before the first gate. Present only the current ticket in ref order: decline skips it; revision rechecks and re-presents its exact diff; approval completes step 9 before the next ticket prints. Never treat a reply as approval of an unseen ticket. Present stopped tickets' draft/report at their turn with no approval option; preserve a split's N-body subset-selection gate. End with the settlement and per-ref rows from `reference/repo-audit.md` after the selected writes finish, including singleton creates.
|
|
116
116
|
|
|
117
117
|
Present, per proposed issue:
|
|
118
118
|
|
|
@@ -206,6 +206,6 @@ Stop on the first nonzero exit without retry or fallback body write. Report the
|
|
|
206
206
|
|
|
207
207
|
Drop runs nothing and ends `<drafted title>: dropped`; create anyway files only the displayed premise-rewritten body with its contradiction in the premise report. Preserve headless stops and split subset selection.
|
|
208
208
|
|
|
209
|
-
Apply a reply only to the current ticket: decline skips it; revision rechecks and re-presents it; approval runs step 9's re-fetch, comparison, and body-then-comment write before the next gate. Treat another ticket's reply as no approval for an unseen diff. Give stopped tickets a hard-stop report without an approval number. Retain the N-body subset-selection gate for a split within one ticket. On an ended run, start the message with one batch settlement line: `gauntlet: done <counters>`. Start the message with `gauntlet: stuck` followed on that line by the dynamic counters instead when any `close-failed`, `close-by-hand`, or `unfetched` count is nonzero. Emit nonzero counters only, comma-joined without spaces, in this fixed order: `written,closed,declined,dropped,no-op,stopped,unfetched,close-failed,close-by-hand`; format each as `<key>=<count>`. Start the message with `gauntlet: done written=1` for an ended singleton whose only outcome is one written ticket; use the same counters rule for every singleton, including `gauntlet: done stopped=1`. Count an integrity stop left without recovery values as `stopped`, not declined; human decline and drop are not failures. While a hard-stop recovery question remains active, emit `gauntlet: question` and no settlement line. Keep ended stopped items settled, without reopening questions or offering new writes.
|
|
209
|
+
Apply a reply only to the current ticket: decline skips it; revision rechecks and re-presents it; approval runs step 9's re-fetch, comparison, and body-then-comment write before the next gate. Treat another ticket's reply as no approval for an unseen diff. Give stopped tickets a hard-stop report without an approval number. Retain the N-body subset-selection gate for a split within one ticket. On an ended run, after every selected write and follow-on step finishes, start the final message with one batch settlement line: `gauntlet: done <counters>`. Start the message with `gauntlet: stuck` followed on that line by the dynamic counters instead when any `close-failed`, `close-by-hand`, or `unfetched` count is nonzero. Emit nonzero counters only, comma-joined without spaces, in this fixed order: `written,closed,declined,dropped,no-op,stopped,unfetched,close-failed,close-by-hand`; format each as `<key>=<count>`. Start the message with `gauntlet: done written=1` for an ended singleton whose only outcome is one written ticket; use the same counters rule for every singleton, including `gauntlet: done stopped=1`. Count an integrity stop left without recovery values as `stopped`, not declined; human decline and drop are not failures. While a hard-stop recovery question remains active, emit `gauntlet: question` and no settlement line. Keep ended stopped items settled, without reopening questions or offering new writes.
|
|
210
210
|
|
|
211
|
-
|
|
211
|
+
Print one plain line per item below the settlement line, in roster order, including singletons: `<ref>: <outcome>`. Use the actual ref returned by the write for a successful create or enrichment; use the drafted title only for a dropped or declined create with no ticket. Keep the outcome exactly one of `written`, `closed`, `close failed (<operation>: <reason>)`, `close by hand`, `dropped`, `declined`, `stopped`, `no-op`, `unfetched`; put other explanations on separate lines. Render rows without bullets, code fences or emphasis.
|