vouchington-tooling 0.43.0 → 0.44.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 +45 -1
- package/dist/cli/commands/fresh-base.d.mts +6 -0
- package/dist/cli/commands/fresh-base.mjs +14 -0
- package/dist/cli/index.mjs +3 -0
- package/dist/cli/parse-fresh-base.d.mts +2 -0
- package/dist/cli/parse-fresh-base.mjs +15 -0
- package/dist/cli/parse.d.mts +5 -0
- package/dist/cli/parse.mjs +3 -0
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +2 -0
- package/dist/fresh-base/classify.d.mts +21 -0
- package/dist/fresh-base/classify.mjs +29 -0
- package/dist/fresh-base/index.d.mts +4 -0
- package/dist/fresh-base/index.mjs +2 -0
- package/dist/fresh-base/run.d.mts +25 -0
- package/dist/fresh-base/run.mjs +66 -0
- package/dist/gh-cli/closing-refs.d.mts +24 -0
- package/dist/gh-cli/closing-refs.mjs +37 -0
- package/dist/gh-cli/index.d.mts +3 -0
- package/dist/gh-cli/index.mjs +2 -0
- package/dist/gh-cli/non-closing-refs.d.mts +11 -0
- package/dist/gh-cli/non-closing-refs.mjs +24 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +3 -0
- package/dist/pull-request-diff/diff-size.d.mts +29 -0
- package/dist/pull-request-diff/diff-size.mjs +35 -0
- package/dist/pull-request-diff/index.d.mts +2 -0
- package/dist/pull-request-diff/index.mjs +1 -0
- package/dist/retrospective-distill/args.d.mts +13 -0
- package/dist/retrospective-distill/args.mjs +27 -0
- package/dist/retrospective-distill/checkpoint-entry.d.mts +2 -0
- package/dist/retrospective-distill/checkpoint-entry.mjs +11 -0
- package/dist/retrospective-distill/fixtures.test-helpers.d.mts +3 -0
- package/dist/retrospective-distill/fixtures.test-helpers.mjs +21 -0
- package/dist/retrospective-distill/flag-args.d.mts +5 -0
- package/dist/retrospective-distill/flag-args.mjs +31 -0
- package/dist/retrospective-distill/index.d.mts +10 -0
- package/dist/retrospective-distill/index.mjs +6 -0
- package/dist/retrospective-distill/partition-records.d.mts +6 -0
- package/dist/retrospective-distill/partition-records.mjs +49 -0
- package/dist/retrospective-distill/retrospective-entry.d.mts +3 -0
- package/dist/retrospective-distill/retrospective-entry.mjs +4 -0
- package/dist/retrospective-distill/run.d.mts +5 -0
- package/dist/retrospective-distill/run.mjs +28 -0
- package/dist/retrospective-distill/session-types.d.mts +17 -0
- package/dist/retrospective-distill/session-types.mjs +1 -0
- package/dist/retrospective-distill/shape.d.mts +29 -0
- package/dist/retrospective-distill/shape.mjs +111 -0
- package/docs/mcp-server.md +1 -1
- package/package.json +18 -3
- package/skills/agent-workflow/SKILL.md +20 -0
- package/skills/agent-workflow/references/four-refs.md +74 -0
- package/skills/blackboard/SKILL.md +21 -1
- package/skills/github-issue/SKILL.md +6 -0
- package/skills/manifest.json +11 -5
- package/skills/postgres-node-performance-tuning/SKILL.md +2 -0
- package/skills/postgres-node-performance-tuning/references/performance-patterns.md +76 -0
- package/skills/postgres-node-performance-tuning/references/reading-plans.md +111 -0
- package/skills/postgres-schema-design/references/relations.md +18 -0
- package/skills/retrospective-distill/SKILL.md +67 -4
- package/skills/revisit-followups/SKILL.md +17 -0
- package/skills/stacked-prs/SKILL.md +47 -16
- package/skills/stacked-prs/references/drain.md +168 -0
- package/skills/stacked-prs/references/gh-stack.md +143 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { parseStringFlags } from "./flag-args.mjs";
|
|
2
|
+
function requireIsoCutoff(value, flag) {
|
|
3
|
+
if (value === undefined)
|
|
4
|
+
throw new Error(`${flag} is required`);
|
|
5
|
+
if (Number.isNaN(Date.parse(value)))
|
|
6
|
+
throw new Error(`${flag} must be a valid ISO 8601 timestamp`);
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Parses `<partition.jsonl> --retro-cutoff <iso> --session-cutoff <iso>`.
|
|
11
|
+
*
|
|
12
|
+
* The classifier never computes its own cutoffs: the eligibility rule is evaluated against
|
|
13
|
+
* whatever `retroCutoff`/`sessionCutoff` the caller passes in, so a classifier run always matches
|
|
14
|
+
* the caller's own idea of "eligible" for that same run.
|
|
15
|
+
*/
|
|
16
|
+
export function parseDistillClassifyArgs(argv) {
|
|
17
|
+
const { values, positional } = parseStringFlags(argv, ['--retro-cutoff', '--session-cutoff']);
|
|
18
|
+
const [partitionPath, ...extra] = positional;
|
|
19
|
+
if (partitionPath === undefined || extra.length > 0) {
|
|
20
|
+
throw new Error('expected exactly one positional argument: the partition JSONL path');
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
partitionPath,
|
|
24
|
+
retroCutoff: requireIsoCutoff(values.get('--retro-cutoff'), '--retro-cutoff'),
|
|
25
|
+
sessionCutoff: requireIsoCutoff(values.get('--session-cutoff'), '--session-cutoff'),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Historic hook-written journal entries stay in the journal until they are archived, even though
|
|
2
|
+
// no hook appends them anymore: agents write their own journal entries. They kept
|
|
3
|
+
// `type: "journal"` and carry this structural `checkpoint` field, which is the only marker that
|
|
4
|
+
// tells one apart from an agent-written entry — the rendered markdown heading is never inspected.
|
|
5
|
+
// Session-shape classification (shape.mts) reads it so those sessions stay `checkpoint-only`.
|
|
6
|
+
const CHECKPOINT_KINDS = ['compaction', 'command-failure', 'pr-create', 'push'];
|
|
7
|
+
const CHECKPOINT_ENTRY_FIELD = 'checkpoint';
|
|
8
|
+
export function isCheckpointEntry(entry) {
|
|
9
|
+
const value = entry.data[CHECKPOINT_ENTRY_FIELD];
|
|
10
|
+
return typeof value === 'string' && CHECKPOINT_KINDS.includes(value);
|
|
11
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function sessionFixture(overrides = {}) {
|
|
2
|
+
return {
|
|
3
|
+
id: 'session-1',
|
|
4
|
+
parentSessionId: null,
|
|
5
|
+
agent: 'claude',
|
|
6
|
+
version: '1',
|
|
7
|
+
createdAt: '2026-07-01T00:00:00.000Z',
|
|
8
|
+
lastEntryAt: null,
|
|
9
|
+
archivedAt: null,
|
|
10
|
+
data: {},
|
|
11
|
+
...overrides,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function entryFixture(overrides = {}) {
|
|
15
|
+
return {
|
|
16
|
+
sessionId: 'session-1',
|
|
17
|
+
createdAt: '2026-07-01T00:00:00.000Z',
|
|
18
|
+
data: {},
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Walks argv for `--flag value` pairs. A value may itself start with a dash (a file path, say), so
|
|
2
|
+
// a missing value is `undefined`, the end-of-options marker, or another recognized flag.
|
|
3
|
+
export function parseStringFlags(argv, flags) {
|
|
4
|
+
const values = new Map();
|
|
5
|
+
const positional = [];
|
|
6
|
+
let optionsEnded = false;
|
|
7
|
+
let consumed = -1;
|
|
8
|
+
for (const [i, arg] of argv.entries()) {
|
|
9
|
+
if (i === consumed)
|
|
10
|
+
continue;
|
|
11
|
+
if (!optionsEnded && arg === '--') {
|
|
12
|
+
optionsEnded = true;
|
|
13
|
+
}
|
|
14
|
+
else if (!optionsEnded && flags.includes(arg)) {
|
|
15
|
+
const value = argv[i + 1];
|
|
16
|
+
if (value === undefined || value === '--' || flags.includes(value))
|
|
17
|
+
throw new Error(`${arg} requires a value`);
|
|
18
|
+
if (values.has(arg))
|
|
19
|
+
throw new Error(`${arg} may only be specified once`);
|
|
20
|
+
values.set(arg, value);
|
|
21
|
+
consumed = i + 1;
|
|
22
|
+
}
|
|
23
|
+
else if (!optionsEnded && arg.startsWith('-')) {
|
|
24
|
+
throw new Error(`unknown option: ${arg}`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
positional.push(arg);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { values, positional };
|
|
31
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { parseDistillClassifyArgs } from './args.mts';
|
|
2
|
+
export type { DistillClassifyArgs } from './args.mts';
|
|
3
|
+
export { isCheckpointEntry } from './checkpoint-entry.mts';
|
|
4
|
+
export { readPartitionRecords } from './partition-records.mts';
|
|
5
|
+
export type { PartitionData } from './partition-records.mts';
|
|
6
|
+
export { isRetrospectiveEntry } from './retrospective-entry.mts';
|
|
7
|
+
export { runDistillClassify } from './run.mts';
|
|
8
|
+
export { classifySession } from './shape.mts';
|
|
9
|
+
export type { DistillCutoffs, SessionClassification, SessionShape } from './shape.mts';
|
|
10
|
+
export type { Session, SessionEntry } from './session-types.mts';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { parseDistillClassifyArgs } from "./args.mjs";
|
|
2
|
+
export { isCheckpointEntry } from "./checkpoint-entry.mjs";
|
|
3
|
+
export { readPartitionRecords } from "./partition-records.mjs";
|
|
4
|
+
export { isRetrospectiveEntry } from "./retrospective-entry.mjs";
|
|
5
|
+
export { runDistillClassify } from "./run.mjs";
|
|
6
|
+
export { classifySession } from "./shape.mjs";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
// Mirrors the record envelope the agent-blackboard client's own partition writer emits: one JSON
|
|
6
|
+
// object per line, discriminated by `type`. A partition file is written read-only only after its
|
|
7
|
+
// own terminal `manifest` line has already validated, so this reader trusts the bytes on disk — it
|
|
8
|
+
// does not re-verify partition integrity, only recognizes the shapes it can use. An unrecognized
|
|
9
|
+
// `type` (a future upstream record kind, or the top-level snapshot stream's own `error` record,
|
|
10
|
+
// which partition files never carry) is skipped rather than treated as fatal, so a
|
|
11
|
+
// forward-compatible upstream release can't break this classifier.
|
|
12
|
+
function parseLine(line) {
|
|
13
|
+
let record;
|
|
14
|
+
try {
|
|
15
|
+
record = JSON.parse(line);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new Error('partition contains a line that is not valid JSON');
|
|
19
|
+
}
|
|
20
|
+
if (!isRecord(record) || typeof record.type !== 'string') {
|
|
21
|
+
throw new Error('partition contains a record with no string "type" field');
|
|
22
|
+
}
|
|
23
|
+
return record;
|
|
24
|
+
}
|
|
25
|
+
export async function readPartitionRecords(path) {
|
|
26
|
+
const raw = await readFile(path, 'utf8');
|
|
27
|
+
const sessions = [];
|
|
28
|
+
const entriesBySession = new Map();
|
|
29
|
+
for (const line of raw.split('\n')) {
|
|
30
|
+
if (line.trim() === '')
|
|
31
|
+
continue;
|
|
32
|
+
const record = parseLine(line);
|
|
33
|
+
if (record.type === 'session' && isRecord(record.session)) {
|
|
34
|
+
sessions.push(record.session);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (record.type === 'entry' && isRecord(record.entry)) {
|
|
38
|
+
const entry = record.entry;
|
|
39
|
+
const existing = entriesBySession.get(entry.sessionId);
|
|
40
|
+
if (existing)
|
|
41
|
+
existing.push(entry);
|
|
42
|
+
else
|
|
43
|
+
entriesBySession.set(entry.sessionId, [entry]);
|
|
44
|
+
}
|
|
45
|
+
// 'manifest' (the partition's own terminal line) and any other record type carry no
|
|
46
|
+
// session-shape information for this classifier and are intentionally ignored.
|
|
47
|
+
}
|
|
48
|
+
return { sessions, entriesBySession };
|
|
49
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure local JSONL parsing plus classification — no network, blackboard client, or writes, so it
|
|
3
|
+
* can run under a read-only inspector sandbox. Returns one tab-separated line per session.
|
|
4
|
+
*/
|
|
5
|
+
export declare function runDistillClassify(argv: string[]): Promise<string>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { parseDistillClassifyArgs } from "./args.mjs";
|
|
2
|
+
import { readPartitionRecords } from "./partition-records.mjs";
|
|
3
|
+
import { classifySession } from "./shape.mjs";
|
|
4
|
+
function formatLine(sessionId, shape, eligible) {
|
|
5
|
+
return `${sessionId}\t${shape}\t${eligible ? 'eligible' : 'not-yet-eligible'}`;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Pure local JSONL parsing plus classification — no network, blackboard client, or writes, so it
|
|
9
|
+
* can run under a read-only inspector sandbox. Returns one tab-separated line per session.
|
|
10
|
+
*/
|
|
11
|
+
export async function runDistillClassify(argv) {
|
|
12
|
+
const args = parseDistillClassifyArgs(argv);
|
|
13
|
+
const { sessions, entriesBySession } = await readPartitionRecords(args.partitionPath);
|
|
14
|
+
if (sessions.length === 0)
|
|
15
|
+
return 'No sessions found in partition.';
|
|
16
|
+
const cutoffs = { retroCutoff: args.retroCutoff, sessionCutoff: args.sessionCutoff };
|
|
17
|
+
return sessions
|
|
18
|
+
.map((session) => {
|
|
19
|
+
const entries = entriesBySession.get(session.id) ?? [];
|
|
20
|
+
const { shape, eligible, quarantine, duplicateEntryCount } = classifySession(session, entries, cutoffs);
|
|
21
|
+
const line = formatLine(session.id, shape, eligible) +
|
|
22
|
+
(duplicateEntryCount ? `\tduplicate-events=${duplicateEntryCount}` : '');
|
|
23
|
+
return quarantine
|
|
24
|
+
? `${line}\tquarantined=${quarantine.entryCount};reasons=${quarantine.reasons.join(',')}`
|
|
25
|
+
: line;
|
|
26
|
+
})
|
|
27
|
+
.join('\n');
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Structurally compatible with the agent-blackboard client's `Session`, so no client install is needed. */
|
|
2
|
+
export interface Session {
|
|
3
|
+
id: string;
|
|
4
|
+
parentSessionId: string | null;
|
|
5
|
+
agent: string;
|
|
6
|
+
version: string;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
lastEntryAt: string | null;
|
|
9
|
+
archivedAt: string | null;
|
|
10
|
+
data: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
/** Structurally compatible with the agent-blackboard client's `SessionEntry`. */
|
|
13
|
+
export interface SessionEntry {
|
|
14
|
+
sessionId: string;
|
|
15
|
+
createdAt: string;
|
|
16
|
+
data: Record<string, unknown>;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Session, SessionEntry } from './session-types.mts';
|
|
2
|
+
/**
|
|
3
|
+
* Session shape taxonomy and distillation eligibility.
|
|
4
|
+
*
|
|
5
|
+
* This code is the source of truth for both. The retrospective-distill skill is the canonical
|
|
6
|
+
* human-facing description; when an inspector's hand classification and this output disagree on
|
|
7
|
+
* the same session, fix the skill to match this code, not the other way around.
|
|
8
|
+
*
|
|
9
|
+
* `zero-entry-child` / `zero-entry-root` name a session-topology fact (whether `parentSessionId`
|
|
10
|
+
* is set), not a completion signal: a delegated child that dies mid-task is a zero-entry-child
|
|
11
|
+
* session too, indistinguishable here from one that finished its bounded assignment cleanly.
|
|
12
|
+
* Docs referencing this split must not imply "child" means "completed normally".
|
|
13
|
+
*/
|
|
14
|
+
export type SessionShape = 'retrospective' | 'entry-type-unresolved' | 'checkpoint-only' | 'journal-only' | 'zero-entry-child' | 'zero-entry-root';
|
|
15
|
+
export type SessionClassification = {
|
|
16
|
+
sessionId: string;
|
|
17
|
+
shape: SessionShape;
|
|
18
|
+
eligible: boolean;
|
|
19
|
+
quarantine?: {
|
|
20
|
+
entryCount: number;
|
|
21
|
+
reasons: string[];
|
|
22
|
+
};
|
|
23
|
+
duplicateEntryCount?: number;
|
|
24
|
+
};
|
|
25
|
+
export type DistillCutoffs = {
|
|
26
|
+
retroCutoff: string;
|
|
27
|
+
sessionCutoff: string;
|
|
28
|
+
};
|
|
29
|
+
export declare function classifySession(session: Session, entries: SessionEntry[], cutoffs: DistillCutoffs): SessionClassification;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
import { validateFeedbackEnvelope } from "../agent-blackboard/index.mjs";
|
|
3
|
+
import { isCheckpointEntry } from "./checkpoint-entry.mjs";
|
|
4
|
+
import { isRetrospectiveEntry } from "./retrospective-entry.mjs";
|
|
5
|
+
function quarantineReason(entry) {
|
|
6
|
+
if (entry.data.type !== 'journal' && entry.data.type !== 'retrospective')
|
|
7
|
+
return 'unknown-entry-type';
|
|
8
|
+
if (entry.data.schemaVersion !== undefined) {
|
|
9
|
+
try {
|
|
10
|
+
validateFeedbackEnvelope(entry.data);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return 'invalid-feedback-envelope';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
function hasUnresolvedType(entry) {
|
|
19
|
+
return quarantineReason(entry) !== undefined;
|
|
20
|
+
}
|
|
21
|
+
function classifyShape(session, entries) {
|
|
22
|
+
if (entries.some(hasUnresolvedType))
|
|
23
|
+
return 'entry-type-unresolved';
|
|
24
|
+
if (entries.some(isRetrospectiveEntry))
|
|
25
|
+
return 'retrospective';
|
|
26
|
+
if (entries.length === 0) {
|
|
27
|
+
// Loose equality on purpose: a partition record missing `parentSessionId` entirely (the reader
|
|
28
|
+
// trusts the file on disk and does not re-validate every field against the Session interface —
|
|
29
|
+
// see partition-records.mts) must fall to zero-entry-root. This is a topology and capture-gap
|
|
30
|
+
// signal, never evidence of crash or clean completion. `=== null` would silently misroute
|
|
31
|
+
// `undefined` into the wrong, discarded bucket.
|
|
32
|
+
return session.parentSessionId == null ? 'zero-entry-root' : 'zero-entry-child';
|
|
33
|
+
}
|
|
34
|
+
return entries.every(isCheckpointEntry) ? 'checkpoint-only' : 'journal-only';
|
|
35
|
+
}
|
|
36
|
+
function newestRetrospectiveAt(entries) {
|
|
37
|
+
let newest;
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (!isRetrospectiveEntry(entry))
|
|
40
|
+
continue;
|
|
41
|
+
if (newest === undefined || Date.parse(entry.createdAt) > Date.parse(newest))
|
|
42
|
+
newest = entry.createdAt;
|
|
43
|
+
}
|
|
44
|
+
return newest;
|
|
45
|
+
}
|
|
46
|
+
// Eligibility rule:
|
|
47
|
+
// (has a retrospective entry AND retroAt < retroCutoff) OR (lastActive < sessionCutoff).
|
|
48
|
+
// An entry-type-unresolved session gets no issue-filing or archival pass "this run, regardless of
|
|
49
|
+
// its age", so it is never eligible — that check short-circuits before the formula.
|
|
50
|
+
function isEligible(session, entries, shape, cutoffs) {
|
|
51
|
+
if (shape === 'entry-type-unresolved')
|
|
52
|
+
return false;
|
|
53
|
+
const retroAt = newestRetrospectiveAt(entries);
|
|
54
|
+
const staleRetro = retroAt !== undefined && Date.parse(retroAt) < Date.parse(cutoffs.retroCutoff);
|
|
55
|
+
const lastActive = entries.reduce((latest, entry) => Date.parse(entry.createdAt) > Date.parse(latest) ? entry.createdAt : latest, session.createdAt);
|
|
56
|
+
const staleSession = Date.parse(lastActive) < Date.parse(cutoffs.sessionCutoff);
|
|
57
|
+
return staleRetro || staleSession;
|
|
58
|
+
}
|
|
59
|
+
// An event is its session, `sourceEventId`, and content; the server assigns `timestamp` per attempt
|
|
60
|
+
// (a retry or a late write beside it can store the same event twice), so it is not identity.
|
|
61
|
+
function withoutTimestamp(data) {
|
|
62
|
+
return Object.fromEntries(Object.entries(data).filter(([key]) => key !== 'timestamp'));
|
|
63
|
+
}
|
|
64
|
+
function normalizeSourceEvents(entries) {
|
|
65
|
+
const events = new Map();
|
|
66
|
+
const unique = [];
|
|
67
|
+
let duplicateEntryCount = 0;
|
|
68
|
+
let conflictCount = 0;
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (entry.data.schemaVersion !== 1 || quarantineReason(entry) !== undefined) {
|
|
71
|
+
unique.push(entry);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const key = `${entry.sessionId}:${String(entry.data.sourceEventId)}`;
|
|
75
|
+
const existing = events.get(key);
|
|
76
|
+
if (!existing) {
|
|
77
|
+
events.set(key, entry);
|
|
78
|
+
unique.push(entry);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!isDeepStrictEqual(withoutTimestamp(existing.data), withoutTimestamp(entry.data))) {
|
|
82
|
+
conflictCount++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
duplicateEntryCount++;
|
|
86
|
+
// At-least-once replay does not postpone a source event's distillation age.
|
|
87
|
+
if (Date.parse(entry.createdAt) < Date.parse(existing.createdAt)) {
|
|
88
|
+
unique[unique.indexOf(existing)] = entry;
|
|
89
|
+
events.set(key, entry);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { unique, duplicateEntryCount, conflictCount };
|
|
93
|
+
}
|
|
94
|
+
export function classifySession(session, entries, cutoffs) {
|
|
95
|
+
const { unique, duplicateEntryCount, conflictCount } = normalizeSourceEvents(entries);
|
|
96
|
+
const shape = conflictCount ? 'entry-type-unresolved' : classifyShape(session, unique);
|
|
97
|
+
const reasons = entries
|
|
98
|
+
.map(quarantineReason)
|
|
99
|
+
.filter((reason) => reason !== undefined);
|
|
100
|
+
for (let index = 0; index < conflictCount; index++)
|
|
101
|
+
reasons.push('conflicting-source-event');
|
|
102
|
+
return {
|
|
103
|
+
sessionId: session.id,
|
|
104
|
+
shape,
|
|
105
|
+
eligible: isEligible(session, unique, shape, cutoffs),
|
|
106
|
+
...(duplicateEntryCount ? { duplicateEntryCount } : {}),
|
|
107
|
+
...(reasons.length === 0
|
|
108
|
+
? {}
|
|
109
|
+
: { quarantine: { entryCount: reasons.length, reasons: [...new Set(reasons)].toSorted() } }),
|
|
110
|
+
};
|
|
111
|
+
}
|
package/docs/mcp-server.md
CHANGED
|
@@ -278,7 +278,7 @@ with the repository's stack tooling instead.
|
|
|
278
278
|
### Tools
|
|
279
279
|
|
|
280
280
|
- `pr_validate` (read-only): renders the body, runs the policy `validatePullRequest({ title, body,
|
|
281
|
-
base, head, mode, pr? })` export and returns `{ body, errors, warnings }`. With `pr`, the live
|
|
281
|
+
base, head, mode, pr? })` export and returns `{ body, errors, warnings }`. With `pr`, the live
|
|
282
282
|
body is read (never written) so its Shepherd Journal is preserved as in `pr_update`.
|
|
283
283
|
- `pr_create` (additive): renders and validates (errors block), then calls the policy
|
|
284
284
|
`pullRequestDefaults({ title, base, relatedIssues })` export for optional `labels`, `milestone`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vouchington-tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "Vouchington CLI and extractable tooling libraries.",
|
|
5
5
|
"homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -101,6 +101,11 @@
|
|
|
101
101
|
"import": "./dist/require-up-to-date/index.mjs",
|
|
102
102
|
"default": "./dist/require-up-to-date/index.mjs"
|
|
103
103
|
},
|
|
104
|
+
"./fresh-base": {
|
|
105
|
+
"types": "./dist/fresh-base/index.d.mts",
|
|
106
|
+
"import": "./dist/fresh-base/index.mjs",
|
|
107
|
+
"default": "./dist/fresh-base/index.mjs"
|
|
108
|
+
},
|
|
104
109
|
"./gitleaks-directory-scan": {
|
|
105
110
|
"types": "./dist/gitleaks-directory-scan/index.d.mts",
|
|
106
111
|
"import": "./dist/gitleaks-directory-scan/index.mjs",
|
|
@@ -136,6 +141,11 @@
|
|
|
136
141
|
"import": "./dist/shared-context/index.mjs",
|
|
137
142
|
"default": "./dist/shared-context/index.mjs"
|
|
138
143
|
},
|
|
144
|
+
"./retrospective-distill": {
|
|
145
|
+
"types": "./dist/retrospective-distill/index.d.mts",
|
|
146
|
+
"import": "./dist/retrospective-distill/index.mjs",
|
|
147
|
+
"default": "./dist/retrospective-distill/index.mjs"
|
|
148
|
+
},
|
|
139
149
|
"./source-module-graph": {
|
|
140
150
|
"types": "./dist/source-module-graph/index.d.mts",
|
|
141
151
|
"import": "./dist/source-module-graph/index.mjs",
|
|
@@ -321,6 +331,11 @@
|
|
|
321
331
|
"import": "./dist/compiler-build/index.mjs",
|
|
322
332
|
"default": "./dist/compiler-build/index.mjs"
|
|
323
333
|
},
|
|
334
|
+
"./pull-request-diff": {
|
|
335
|
+
"types": "./dist/pull-request-diff/index.d.mts",
|
|
336
|
+
"import": "./dist/pull-request-diff/index.mjs",
|
|
337
|
+
"default": "./dist/pull-request-diff/index.mjs"
|
|
338
|
+
},
|
|
324
339
|
"./github-projects": {
|
|
325
340
|
"types": "./dist/github-projects/index.d.mts",
|
|
326
341
|
"import": "./dist/github-projects/index.mjs",
|
|
@@ -364,7 +379,7 @@
|
|
|
364
379
|
"dependencies": {
|
|
365
380
|
"@types/mdast": "4.0.4",
|
|
366
381
|
"@types/unist": "3.0.3",
|
|
367
|
-
"@typescript-eslint/parser": "^8.71.
|
|
382
|
+
"@typescript-eslint/parser": "^8.71.1",
|
|
368
383
|
"csv-parse": "7.0.3",
|
|
369
384
|
"csv-stringify": "6.9.0",
|
|
370
385
|
"dockerfile-ast": "0.7.1",
|
|
@@ -377,7 +392,7 @@
|
|
|
377
392
|
},
|
|
378
393
|
"devDependencies": {
|
|
379
394
|
"@ast-grep/cli": "0.45.3",
|
|
380
|
-
"@modelcontextprotocol/sdk": "1.32.
|
|
395
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
381
396
|
"@types/picomatch": "^4.0.3",
|
|
382
397
|
"@types/spdx-expression-parse": "4.0.0",
|
|
383
398
|
"agent-blackboard": "^0.6.1",
|
|
@@ -22,10 +22,30 @@ evidence record.
|
|
|
22
22
|
- Before handoff, use [evidence sweep](references/evidence-sweep.md).
|
|
23
23
|
- For review, use [review](references/review.md).
|
|
24
24
|
- For surfaced feedback, use [review response](references/review-response.md).
|
|
25
|
+
- Before any rebase or lease push, use [four refs](references/four-refs.md).
|
|
25
26
|
|
|
26
27
|
Load only the references required by the current phase. Keep scope changes explicit; a discovered
|
|
27
28
|
blocker or follow-up does not authorize widening the task.
|
|
28
29
|
|
|
30
|
+
## Close-out
|
|
31
|
+
|
|
32
|
+
This section is the single home for the hand-off retrospective and merge-note rules.
|
|
33
|
+
|
|
34
|
+
- Report actual validation, open pull requests, and remaining blockers.
|
|
35
|
+
- **Hand-off retrospective.** When you hand a pull request to the human as ready, write one
|
|
36
|
+
[retrospective](../retrospective/SKILL.md) from the session that did the work, which reads that
|
|
37
|
+
session's transcripts and journal. A native stack gets one retrospective when the stack is handed
|
|
38
|
+
off, covering every layer; never one per layer or for an intermediate commit.
|
|
39
|
+
- Give each hand-off its own `sourceEventId`, such as `handoff-pr<number>` (a stack uses its top
|
|
40
|
+
pull request's number), on both `retrospective_check` and `retrospective_save`. The default ID
|
|
41
|
+
fits only the first hand-off in a session; a second would be mistaken for it.
|
|
42
|
+
- **Merge note.** If you are still running when the merge happens and it changes the outcome, such
|
|
43
|
+
as a merge-queue ejection and its fix, a follow-up push, or review comments addressed after
|
|
44
|
+
hand-off, append one short note with the [blackboard skill](../blackboard/SKILL.md). Name the
|
|
45
|
+
pull request and the retrospective's source event, and give the note its own `sourceEventId` (for
|
|
46
|
+
example `merge-pr<number>`): the writer rejects the retrospective's ID reused for different
|
|
47
|
+
content. Never write a second retrospective.
|
|
48
|
+
|
|
29
49
|
## Parallel by default
|
|
30
50
|
|
|
31
51
|
Split work into independent units and run them concurrently, one worker and worktree each, within
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Reading the four refs
|
|
2
|
+
|
|
3
|
+
Use before any rebase or lease push of a branch other agents or humans may also write. Publish this
|
|
4
|
+
branch; do not open a replacement pull request.
|
|
5
|
+
|
|
6
|
+
`origin` and `main` below stand for the consumer's remote and default branch; substitute the names
|
|
7
|
+
local policy declares.
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
- Run `git fetch origin main <branch>` as its own command. Never chain the fetch with a rebase,
|
|
12
|
+
reset, or merge.
|
|
13
|
+
- HEAD must be the branch being updated. A stack checkout can leave the top layer checked out, so
|
|
14
|
+
check out the layer first. A missing stack field means membership is unknown, not unstacked.
|
|
15
|
+
- A dirty worktree stops you before a fast-forward, reset, or rebase. Do not switch branches.
|
|
16
|
+
- Continue a conflicted rebase with `GIT_EDITOR=true git rebase --continue`.
|
|
17
|
+
|
|
18
|
+
## Read the refs
|
|
19
|
+
|
|
20
|
+
- Print `git rev-parse` of HEAD, `origin/<branch>`, `origin/main`, and local `main` when it exists.
|
|
21
|
+
Local `main` is never the rebase target; when it exists, `git rev-list --left-right --count
|
|
22
|
+
main...origin/main` with a nonzero right count means local `main` is behind.
|
|
23
|
+
- Print `git rev-list --left-right --count HEAD...origin/<branch>` (left: commits only on the
|
|
24
|
+
local branch; right: commits only on `origin/<branch>`) and
|
|
25
|
+
`git log --oneline --left-right HEAD...origin/<branch>`.
|
|
26
|
+
- Print `git cherry -v origin/<branch> HEAD` and `git cherry -v HEAD origin/<branch>`. A leading `-`
|
|
27
|
+
is the same patch under another SHA; a leading `+` is a patch the other side lacks.
|
|
28
|
+
- Treat `git range-diff` as a reading aid, not the rebase cut, and read it from the base those
|
|
29
|
+
commits were built on. Never run it from a new stack parent: after a stack rebase, old parent
|
|
30
|
+
commits look like unique local work.
|
|
31
|
+
- Unmatched commits on `origin/<branch>` are new commits someone pushed.
|
|
32
|
+
|
|
33
|
+
## Lease push
|
|
34
|
+
|
|
35
|
+
- Capture `<sha>` with `git rev-parse origin/<branch>` after the fetch and before any rewrite. Push
|
|
36
|
+
a non-fast-forward with `git push --force-with-lease=<branch>:<sha> origin <branch>`, always
|
|
37
|
+
naming the remote and branch so a configured push remote cannot change the destination. With concurrent writers,
|
|
38
|
+
capture right after the setup fetch and before any later fetch, so a later fetch cannot silently
|
|
39
|
+
widen the lease.
|
|
40
|
+
- If HEAD equals that SHA, do not push.
|
|
41
|
+
- If a lease or non-force push is rejected, fetch again and re-read. It is still this branch.
|
|
42
|
+
|
|
43
|
+
## Unstacked
|
|
44
|
+
|
|
45
|
+
Before any move and after every move onto `origin/<branch>`, including when HEAD already equals it
|
|
46
|
+
(`0 0`) but `origin/main` has advanced, run `git merge-base --is-ancestor origin/main HEAD`. Exit 0
|
|
47
|
+
means the branch already contains `origin/main`; nonzero means rebase onto `origin/main`, then lease
|
|
48
|
+
push.
|
|
49
|
+
|
|
50
|
+
| Left/right | Action |
|
|
51
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
52
|
+
| `0 0`, `origin/main` already an ancestor | Stop. |
|
|
53
|
+
| `0 0`, `origin/main` not an ancestor | Rebase onto `origin/main`, then lease push. |
|
|
54
|
+
| `0 N` | `git merge --ff-only origin/<branch>`, then the ancestor check. |
|
|
55
|
+
| `N 0` | Read `git log --oneline origin/<branch>..HEAD`: those commits are what a push adds, including a remote rewind to an ancestor. Own unpushed work with `origin/main` an ancestor: `git push origin <branch>`. Otherwise rebase onto `origin/main`, then lease push. A rewind you did not make: stop and ask. |
|
|
56
|
+
| Diverged, local-only commits are rewritten copies (every local-only commit has cherry `-`; matching subjects alone do not prove equivalence), no unique local work | Clean tree, `git reset --hard origin/<branch>`, then the ancestor check. |
|
|
57
|
+
| Diverged, origin has new patches to keep and HEAD has unique local commits | `git rebase origin/<branch>` as its own command, then the ancestor check, then lease push whichever way the check exits (after another rebase onto `origin/main` if it is nonzero). This is the one added rebase target, not a license to rebase onto an arbitrary ref. If the rewrite changed patches and unique local work is mixed in, stop and ask. |
|
|
58
|
+
|
|
59
|
+
## Stacked
|
|
60
|
+
|
|
61
|
+
A stack rebase is the usual reason `origin/<branch>` was rewritten, but another agent or a human can
|
|
62
|
+
rewrite an unstacked branch the same way.
|
|
63
|
+
|
|
64
|
+
- On a diverged layer, adopt `origin/<branch>` with `git reset --hard` when the layer is checked
|
|
65
|
+
out, or `git branch -f` when it is not, if the left-right log shows pre-rebase copies: the same
|
|
66
|
+
layer subjects on both sides and old parent subjects only on the left. Require `git cherry`
|
|
67
|
+
patch equivalence (`-`) for every local layer commit first; matching subjects alone do not
|
|
68
|
+
prove it, and a `+` commit is unique local work.
|
|
69
|
+
- Never `git rebase origin/<branch>` on a stack layer: it replays parent commits onto the layer.
|
|
70
|
+
- Never run the plain rebase-onto-main procedure on a mid-stack branch; use its stack-aware form.
|
|
71
|
+
- If the layer has unpushed commits that are not the old parent and not already on origin under a
|
|
72
|
+
new SHA, stop and ask.
|
|
73
|
+
- After adopting the rewritten tip, run the stack-aware rebase only when the stack still needs to
|
|
74
|
+
move onto `origin/main`.
|
|
@@ -50,7 +50,10 @@ human which case applies before continuing:
|
|
|
50
50
|
registration and diagnosis steps.
|
|
51
51
|
- Registered but disconnected: reconnect it through the harness's MCP controls, or restart the
|
|
52
52
|
harness.
|
|
53
|
-
- Automated runs with no human to ask:
|
|
53
|
+
- Automated runs with no human to ask: an unattended `interactive` run continues its primary work,
|
|
54
|
+
while an `autonomous` run keeps its admission gate below. The final report states why journaling
|
|
55
|
+
was unavailable, the `pendingCount` and `worktreePendingCount` (or that none was written), and
|
|
56
|
+
each unjournaled finding.
|
|
54
57
|
|
|
55
58
|
Prefer restoring MCP over the CLI fallback below, and use the fallback only when the human asks or
|
|
56
59
|
MCP cannot be restored for this session. Harness sandboxes commonly withhold
|
|
@@ -176,5 +179,22 @@ bounded structured facts or sanitized summaries, never raw command output, trans
|
|
|
176
179
|
environment dumps, provider payloads, or secrets. Known-secret redaction supplements the author's
|
|
177
180
|
minimization responsibility; it cannot prove arbitrary prose contains no undisclosed secret.
|
|
178
181
|
|
|
182
|
+
## Credential failures
|
|
183
|
+
|
|
184
|
+
- Never search shell profiles, `env`, or `.env*` files for a credential. Check presence only with
|
|
185
|
+
`[ -n "${VAR+x}" ] && echo SET || echo UNSET`; never copy the token around a sandbox denial.
|
|
186
|
+
- A missing or stale credential blocks hosted delivery. In interactive mode the outbox keeps the
|
|
187
|
+
record visibly pending: do not drop it or invent a fallback. Ask the human to refresh the
|
|
188
|
+
connection, then restart the MCP client or call `outbox_flush`; an automated run reports.
|
|
189
|
+
|
|
190
|
+
## Child identity for spawned agents
|
|
191
|
+
|
|
192
|
+
- Pass the parent's own session id in the assignment prompt. The child resolves its own id from its
|
|
193
|
+
runtime through the consumer's identity policy, then calls `session_ensure` with it, the given
|
|
194
|
+
`parentSessionId`, its `agent`, and its version before other work. With no id or a failed ensure,
|
|
195
|
+
it stops and reports the blocker; never guess an id or pass a path, URL, or synthesized value.
|
|
196
|
+
- A subagent sharing its parent's id must not `session_ensure` it as a child. It asks the parent
|
|
197
|
+
to append, or skips this protocol.
|
|
198
|
+
|
|
179
199
|
Consumer wrappers own service configuration, server registration and approval rules, trusted mode
|
|
180
200
|
selection, issue routing, and archival authorization. The server fixes the outbox location.
|
|
@@ -99,5 +99,11 @@ For a batch, preflight every entry before writing any issue. One invalid entry f
|
|
|
99
99
|
batch. A partial transport failure stops further creation and reports every issue already created.
|
|
100
100
|
Do not retry an entry that succeeded.
|
|
101
101
|
|
|
102
|
+
## Bounded output
|
|
103
|
+
|
|
104
|
+
- A search returns at most five `#N: title -- url` lines.
|
|
105
|
+
- A near-duplicate returns `Duplicate of #N: <url>`.
|
|
106
|
+
- A mutation returns the action, URL, labels, and verification, plus milestone and project when set.
|
|
107
|
+
|
|
102
108
|
Consumer wrapper owns: repository allowlist, taxonomy, issue template, credential, and CLI wrapper.
|
|
103
109
|
It may tighten this gate and may not weaken it.
|