Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions docs/gists/auto-git.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Gist file mapping:
- Uses a compact snapshot helper to detect Git index write capability, run locks, package-manager hints, ledger occupancy, PR handoffs, PR readiness, and the recommended verification profile before expensive commands.
- Writes a sanitized start decision receipt on claimed runs, including the normalized route, selected workflow, required gates, branch/worktree context, release-preflight requirement, and follow-up thread handoff requirement without storing raw transcript text.
- Supports two workflows: local review for single-chat code review and coordinated branch for multi-chat conflicts, PR handoffs, experiments, and fanouts.
- Tracks cooperative run state and PR handoffs under `~/.async/auto-git/v1/repos/<repo-hash>/ledger.json`, with live leases under `~/.async/locks/auto-git/`, without storing raw diffs, prompts, full command output, environment dumps, or secrets.
- Tracks cooperative run state, PR handoffs, and sanitized thread handoff metadata under `~/.async/auto-git/v1/repos/<repo-hash>/ledger.json`, with live leases under `~/.async/locks/auto-git/`, without storing raw diffs, prompts, transcripts, full command output, environment dumps, secrets, or local absolute worktree paths in handoff records.
- Commits by change intent instead of making one vague bulk commit.
- Reads the code and diffs when there are many unstaged changes, then builds the best commit split.
- Optionally routes large or unclear worktrees to `git-intent-audit` before committing.
Expand Down Expand Up @@ -331,6 +331,7 @@ Controller helpers:
```sh
auto-git start --cwd "$PWD" --task "fix this"
auto-git ledger list --cwd "$PWD"
auto-git ledger record-thread --cwd "$PWD" --run-id "<id>" --action create --thread-id "<thread-id>" --target "ADR 4" --repo "async/auto-git" --package "@async/auto-git" --next-adr "ADR 4"
auto-git finish --cwd "$PWD" --run-id "<id>" --complete
auto-git release-preflight --cwd "$PWD" --run-id "<id>" --require-verification
```
Expand All @@ -343,13 +344,21 @@ explicit deferral, follow-up thread handoff, return to main/default, and the
final ledger update. Blockers are short command-class hints and do not include
raw diffs, command output, transcripts, or secret-looking values.

Use `auto-git ledger record-thread` when a coordinated follow-up route creates,
sends to, reads, or hands off another thread. The record keeps only sanitized
coordination metadata: source session id when available, thread id, action,
target ADR or work item, repository/package labels, branch, worktree class or
basename, PR reference, release-check status, and the next ADR label. It rejects
or excludes transcript-like and secret-looking values.

For release and yolo routes, `auto-git release-preflight` records successful
preflight evidence to the active or requested run using safe metadata only.
If release execution is intentionally deferred, finish requires an explicit
`--defer-release`.

Completion from main/default still preserves the completed branch/head in the
ledger so later chats can find the exact handoff.
ledger so later chats can find the exact handoff. When thread handoff evidence
is present, finish preserves that sanitized metadata through completion.

For long or environment-sensitive gates, use:

Expand Down
13 changes: 11 additions & 2 deletions gists/auto-git/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Gist file mapping:
- Uses a compact snapshot helper to detect Git index write capability, run locks, package-manager hints, ledger occupancy, PR handoffs, PR readiness, and the recommended verification profile before expensive commands.
- Writes a sanitized start decision receipt on claimed runs, including the normalized route, selected workflow, required gates, branch/worktree context, release-preflight requirement, and follow-up thread handoff requirement without storing raw transcript text.
- Supports two workflows: local review for single-chat code review and coordinated branch for multi-chat conflicts, PR handoffs, experiments, and fanouts.
- Tracks cooperative run state and PR handoffs under `~/.async/auto-git/v1/repos/<repo-hash>/ledger.json`, with live leases under `~/.async/locks/auto-git/`, without storing raw diffs, prompts, full command output, environment dumps, or secrets.
- Tracks cooperative run state, PR handoffs, and sanitized thread handoff metadata under `~/.async/auto-git/v1/repos/<repo-hash>/ledger.json`, with live leases under `~/.async/locks/auto-git/`, without storing raw diffs, prompts, transcripts, full command output, environment dumps, secrets, or local absolute worktree paths in handoff records.
- Commits by change intent instead of making one vague bulk commit.
- Reads the code and diffs when there are many unstaged changes, then builds the best commit split.
- Optionally routes large or unclear worktrees to `git-intent-audit` before committing.
Expand Down Expand Up @@ -331,6 +331,7 @@ Controller helpers:
```sh
auto-git start --cwd "$PWD" --task "fix this"
auto-git ledger list --cwd "$PWD"
auto-git ledger record-thread --cwd "$PWD" --run-id "<id>" --action create --thread-id "<thread-id>" --target "ADR 4" --repo "async/auto-git" --package "@async/auto-git" --next-adr "ADR 4"
auto-git finish --cwd "$PWD" --run-id "<id>" --complete
auto-git release-preflight --cwd "$PWD" --run-id "<id>" --require-verification
```
Expand All @@ -343,13 +344,21 @@ explicit deferral, follow-up thread handoff, return to main/default, and the
final ledger update. Blockers are short command-class hints and do not include
raw diffs, command output, transcripts, or secret-looking values.

Use `auto-git ledger record-thread` when a coordinated follow-up route creates,
sends to, reads, or hands off another thread. The record keeps only sanitized
coordination metadata: source session id when available, thread id, action,
target ADR or work item, repository/package labels, branch, worktree class or
basename, PR reference, release-check status, and the next ADR label. It rejects
or excludes transcript-like and secret-looking values.

For release and yolo routes, `auto-git release-preflight` records successful
preflight evidence to the active or requested run using safe metadata only.
If release execution is intentionally deferred, finish requires an explicit
`--defer-release`.

Completion from main/default still preserves the completed branch/head in the
ledger so later chats can find the exact handoff.
ledger so later chats can find the exact handoff. When thread handoff evidence
is present, finish preserves that sanitized metadata through completion.

For long or environment-sensitive gates, use:

Expand Down
13 changes: 11 additions & 2 deletions gists/auto-git/auto-git.SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,13 @@ PATH, use the installed skill's `scripts/*.mjs` helper paths as a fallback.
- prints active runs, stale runs, completed runs, PR handoffs, branches,
worktrees, leases, decision receipts, and verification state from safe
ledger metadata
- `auto-git ledger record-thread --cwd "$PWD" --run-id "<id>" --action <create|send|read|handoff> [--thread-id "<id>"] [--source-session "<id>"] [--target "<ADR or work item>"] [--repo "<owner/repo>"] [--package "<package>"] [--branch "<branch>"] [--worktree "<path or label>"] [--pr-url "<url>"] [--pr-number "<n>"] [--release-check <not-in-scope|passed|failed|blocked|deferred|unknown>] [--next-adr "<label>"]`
- records sanitized follow-up thread handoff metadata on a run without
storing prompts, transcripts, raw command output, environment values,
secrets, or local absolute worktree paths
- stores only thread ids, action type, source session id when available,
target work label, repository/package labels, branch, worktree class or
basename, PR reference, release-check status, and next ADR label
- never deletes ledger entries
- `auto-git finish --cwd "$PWD" --run-id "<id>" [--complete]`
- checks dirty state, unresolved index state, HEAD/upstream, active run
Expand All @@ -261,7 +268,9 @@ PATH, use the installed skill's `scripts/*.mjs` helper paths as a fallback.
- blocks release/yolo completion until release-preflight evidence is recorded
and release execution is recorded or explicitly deferred with
`--defer-release`
- blocks follow-up-thread completion until thread handoff evidence exists
- blocks follow-up-thread completion until thread handoff evidence exists,
and preserves sanitized thread handoff metadata when completion writes the
final ledger receipt
- preserves the completed branch/head in the ledger even when completion is
run from main/default after cleanup
- records PR metadata when asked and completes the run only when safe
Expand All @@ -277,7 +286,7 @@ PATH, use the installed skill's `scripts/*.mjs` helper paths as a fallback.

## Global Async State

Auto Git may use global advisory state under `~/.async/auto-git/v1/repos/<repo-hash>/` to avoid repeating expensive inspection and to coordinate across chats. Live runtime leases use Async-compatible lock records under `~/.async/locks/auto-git/repos/<repo-hash>/runs/*.lease.json`; completion removes the live lease and writes a receipt under `~/.async/locks/auto-git/history/`. This state is a cache of safe metadata: fingerprints, file path lists, commit ids, command names, exit codes, timestamps, lock classifications, process ids started by Auto Git, execution profiles, generated env override names/values, durations, recovery hints, run ids, task slugs, lifecycle modes, coordinated intents, branch names, worktree paths, base branches, lease expirations, lease paths, verification keys, release-preflight evidence, release deferral state, thread handoff ids/status, and PR URLs/statuses.
Auto Git may use global advisory state under `~/.async/auto-git/v1/repos/<repo-hash>/` to avoid repeating expensive inspection and to coordinate across chats. Live runtime leases use Async-compatible lock records under `~/.async/locks/auto-git/repos/<repo-hash>/runs/*.lease.json`; completion removes the live lease and writes a receipt under `~/.async/locks/auto-git/history/`. This state is a cache of safe metadata: fingerprints, file path lists, commit ids, command names, exit codes, timestamps, lock classifications, process ids started by Auto Git, execution profiles, generated env override names/values, durations, recovery hints, run ids, task slugs, lifecycle modes, coordinated intents, branch names, worktree paths, base branches, lease expirations, lease paths, verification keys, release-preflight evidence, release deferral state, sanitized thread handoff action/source/thread/target/repo/package/branch/worktree-class/PR/release-check/next-ADR metadata, and PR URLs/statuses.

The ledger is cooperative. Auto Git can reliably detect stale or inactive chats
only when those chats used Auto Git and wrote ledger state. A run is active
Expand Down
111 changes: 109 additions & 2 deletions gists/auto-git/auto-git.script-auto-git-finish.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#!/usr/bin/env node
import { existsSync, readFileSync } from "node:fs";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { spawnSync } from "node:child_process";
import { join, resolve } from "node:path";
import { basename, isAbsolute, join, resolve } from "node:path";

const SNAPSHOT_SCRIPT = new URL("./auto-git-snapshot.mjs", import.meta.url);
const THREAD_ACTIONS = ["create", "send", "read", "handoff"];
const RELEASE_CHECK_STATUSES = ["not-in-scope", "passed", "failed", "blocked", "deferred", "unknown"];

function usage() {
return [
Expand Down Expand Up @@ -112,6 +114,10 @@ function readJson(path, fallback) {
}
}

function writeJson(path, value) {
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}

function currentRun(snapshot, requestedId) {
const runs = [
...(snapshot.occupancy?.activeRuns ?? []),
Expand Down Expand Up @@ -219,6 +225,102 @@ function git(cwd, args) {
return spawnSync("git", args, { cwd, encoding: "utf8" });
}

function looksSecretish(value) {
return /(?:TOKEN|SECRET|PASSWORD|PASSWD|_authToken|ACCESS_KEY|PRIVATE_KEY)\s*[=:]/i.test(String(value ?? ""));
}

function looksTranscriptish(value) {
const text = String(value ?? "");
return /(?:BEGIN TRANSCRIPT|END TRANSCRIPT|<codex_delegation>|<conversation|raw transcript|full transcript|full prompt|assistant:|user:)/i.test(
text
);
}

function safeText(value, maxLength = 120) {
if (typeof value !== "string") return undefined;
const text = value.trim().replace(/\s+/g, " ");
if (!text || looksSecretish(text) || looksTranscriptish(text)) return undefined;
return text.slice(0, maxLength);
}

function safeLabel(value, maxLength = 120) {
const text = safeText(value, maxLength);
if (!text) return undefined;
if (isAbsolute(text)) return basename(text).slice(0, maxLength);
return text;
}

function safePrUrl(value) {
const text = safeText(value, 300);
if (!text || /[?&](?:token|auth|secret|password|key)=/i.test(text)) return undefined;
try {
const parsed = new URL(text);
if (parsed.username || parsed.password) return undefined;
} catch {
return undefined;
}
return text;
}

function sanitizeThreadHandoff(handoff) {
if (!handoff || typeof handoff !== "object") return undefined;
const action = THREAD_ACTIONS.includes(handoff.action) ? handoff.action : undefined;
const status = safeText(handoff.status, 40);
const threadId = safeText(handoff.threadId, 120);
const prUrl = safePrUrl(handoff.pr?.url);
const prNumber = Number.isInteger(Number(handoff.pr?.number)) ? Number(handoff.pr.number) : undefined;
const releaseStatus = RELEASE_CHECK_STATUSES.includes(handoff.releaseCheck?.status)
? handoff.releaseCheck.status
: undefined;
const worktree =
handoff.worktree && typeof handoff.worktree === "object"
? {
class: safeText(handoff.worktree.class, 80),
basename: safeText(handoff.worktree.basename, 80)
}
: undefined;
const sanitized = {
schemaVersion: 1,
status: status ?? (threadId || action ? "recorded" : undefined),
action,
sourceSessionId: safeText(handoff.sourceSessionId, 120),
threadId,
target: safeLabel(handoff.target, 120),
repository: safeLabel(handoff.repository, 120),
package: safeLabel(handoff.package, 120),
branch: safeLabel(handoff.branch, 160),
worktree: worktree?.class || worktree?.basename ? worktree : undefined,
pr: prUrl || prNumber ? { url: prUrl, number: prNumber } : undefined,
releaseCheck: releaseStatus ? { status: releaseStatus } : undefined,
nextAdr: safeLabel(handoff.nextAdr, 120),
recordedAt: typeof handoff.recordedAt === "string" ? handoff.recordedAt : undefined
};
return Object.fromEntries(Object.entries(sanitized).filter(([, value]) => value !== undefined));
}

function rawLedgerPath(snapshot) {
return join(stateRoot(), "repos", snapshot.repo.hash, "ledger.json");
}

function readRawRun(snapshot, runId) {
const ledger = readJson(rawLedgerPath(snapshot), { runs: [] });
return Array.isArray(ledger.runs) ? ledger.runs.find((entry) => entry?.id === runId) : undefined;
}

function preserveThreadHandoff(snapshot, runId, handoff) {
const sanitized = sanitizeThreadHandoff(handoff);
if (!sanitized) return false;
const path = rawLedgerPath(snapshot);
const ledger = readJson(path, { runs: [] });
if (!Array.isArray(ledger.runs)) return false;
const index = ledger.runs.findIndex((entry) => entry?.id === runId);
if (index === -1) return false;
const runs = [...ledger.runs];
runs[index] = { ...runs[index], threadHandoff: sanitized };
writeJson(path, { ...ledger, runs });
return true;
}

function defaultBaseBranch(snapshot, run) {
if (run?.baseBranch) return run.baseBranch;
const remoteHead = snapshot.topology.defaultRemoteHead;
Expand Down Expand Up @@ -610,6 +712,7 @@ function buildReceipt(options) {
let snapshot = inspect(cwd, options.runId);
let run = currentRun(snapshot, options.runId);
const runId = options.runId ?? run?.id;
const rawThreadHandoff = runId ? readRawRun(snapshot, runId)?.threadHandoff : undefined;
const mutations = [];

if (options.recordPr) {
Expand Down Expand Up @@ -650,6 +753,10 @@ function buildReceipt(options) {
mutations.push("complete-run");
completed = true;
snapshot = inspect(cwd, runId);
if (preserveThreadHandoff(snapshot, runId, rawThreadHandoff)) {
mutations.push("preserve-thread-handoff");
snapshot = inspect(cwd, runId);
}
}
const ledger = ledgerStatus(snapshot, runId);

Expand Down
Loading
Loading