dsh-pstack 0.0.0-stage → 0.2.0-beta.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/LICENSE +22 -0
- package/README.md +133 -2
- package/assets/agents/comment-sicko.md +32 -0
- package/assets/agents/poteto-agent.md +8 -0
- package/assets/docs/guide/01-setup.md +64 -0
- package/assets/docs/guide/02-poteto-mode.md +139 -0
- package/assets/docs/guide/03-understand.md +79 -0
- package/assets/docs/guide/04-design.md +133 -0
- package/assets/docs/guide/05-build-and-clean.md +83 -0
- package/assets/docs/guide/06-verify-and-ship.md +152 -0
- package/assets/docs/guide/07-overnight.md +120 -0
- package/assets/docs/guide/08-principles.md +72 -0
- package/assets/docs/guide/09-make-it-yours.md +96 -0
- package/assets/docs/guide/10-recipes-and-pitfalls.md +180 -0
- package/assets/docs/guide/README.md +38 -0
- package/assets/docs/guide/images/design.jpg +0 -0
- package/assets/docs/guide/images/overnight.jpg +0 -0
- package/assets/docs/guide/images/recipes.jpg +0 -0
- package/assets/docs/guide/images/router.jpg +0 -0
- package/assets/docs/guide/images/understanding.jpg +0 -0
- package/assets/docs/guide/images/verification.jpg +0 -0
- package/assets/docs/harness/dsh.md +60 -0
- package/assets/docs/pstack-models.example.md +54 -0
- package/assets/skills/architect/SKILL.md +81 -0
- package/assets/skills/architect/references/design-red-flags.md +57 -0
- package/assets/skills/architect/references/rationale-template.md +35 -0
- package/assets/skills/architect/references/runner-prompt.md +20 -0
- package/assets/skills/arena/SKILL.md +50 -0
- package/assets/skills/automate-me/SKILL.md +106 -0
- package/assets/skills/benchmark-checklist/SKILL.md +39 -0
- package/assets/skills/blast-radius/SKILL.md +52 -0
- package/assets/skills/bro/SKILL.md +7 -0
- package/assets/skills/correct/SKILL.md +32 -0
- package/assets/skills/create-formal-verification-skill/SKILL.md +43 -0
- package/assets/skills/create-verification-skill/SKILL.md +46 -0
- package/assets/skills/create-verification-skill/references/feature-map-example/README.md +47 -0
- package/assets/skills/create-verification-skill/references/feature-map-example/create-note.md +39 -0
- package/assets/skills/create-verification-skill/references/feature-map-example/search.md +45 -0
- package/assets/skills/figure-it-out/SKILL.md +54 -0
- package/assets/skills/how/SKILL.md +38 -0
- package/assets/skills/how/references/explainer-prompt.md +55 -0
- package/assets/skills/how/references/explorer-prompt.md +35 -0
- package/assets/skills/in-your-own-words/SKILL.md +7 -0
- package/assets/skills/interrogate/SKILL.md +71 -0
- package/assets/skills/interrogate/references/code-quality-review.md +47 -0
- package/assets/skills/interrogate/references/lead-judgment.md +39 -0
- package/assets/skills/interrogate/references/reviewer-prompt.md +62 -0
- package/assets/skills/interrogate/references/rubric.md +77 -0
- package/assets/skills/ketchup/SKILL.md +45 -0
- package/assets/skills/maintain-formal-verification-skill/SKILL.md +60 -0
- package/assets/skills/maintain-verification-skill/SKILL.md +41 -0
- package/assets/skills/no-comments/SKILL.md +26 -0
- package/assets/skills/poteto-help/SKILL.md +162 -0
- package/assets/skills/poteto-help/references/prompting.md +51 -0
- package/assets/skills/poteto-help/references/recipes.md +51 -0
- package/assets/skills/poteto-mode/SKILL.md +151 -0
- package/assets/skills/poteto-mode/playbooks/authoring-a-skill.md +12 -0
- package/assets/skills/poteto-mode/playbooks/autonomous-run.md +15 -0
- package/assets/skills/poteto-mode/playbooks/autopilot-full.md +25 -0
- package/assets/skills/poteto-mode/playbooks/autopilot-stack.md +12 -0
- package/assets/skills/poteto-mode/playbooks/babysit.md +23 -0
- package/assets/skills/poteto-mode/playbooks/bug-fix.md +15 -0
- package/assets/skills/poteto-mode/playbooks/eval.md +25 -0
- package/assets/skills/poteto-mode/playbooks/feature.md +21 -0
- package/assets/skills/poteto-mode/playbooks/hillclimb.md +21 -0
- package/assets/skills/poteto-mode/playbooks/investigation.md +14 -0
- package/assets/skills/poteto-mode/playbooks/multi-phase-plan.md +155 -0
- package/assets/skills/poteto-mode/playbooks/opening-a-pr.md +34 -0
- package/assets/skills/poteto-mode/playbooks/orchestrate.md +112 -0
- package/assets/skills/poteto-mode/playbooks/pause-safely.md +10 -0
- package/assets/skills/poteto-mode/playbooks/perf-issue.md +25 -0
- package/assets/skills/poteto-mode/playbooks/prototype.md +14 -0
- package/assets/skills/poteto-mode/playbooks/refactoring.md +16 -0
- package/assets/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
- package/assets/skills/poteto-mode/playbooks/session-pickup.md +11 -0
- package/assets/skills/poteto-mode/playbooks/shipping.md +12 -0
- package/assets/skills/poteto-mode/playbooks/trace-forensics.md +14 -0
- package/assets/skills/poteto-mode/playbooks/visual-parity.md +11 -0
- package/assets/skills/poteto-mode/playbooks/worktree-cleanup.md +20 -0
- package/assets/skills/poteto-mode/references/bugbot-triage.md +142 -0
- package/assets/skills/poteto-mode/scripts/check-plan.mjs +220 -0
- package/assets/skills/poteto-mode/scripts/check-plan.test.mjs +343 -0
- package/assets/skills/poteto-mode/scripts/orch/orch.mjs +4282 -0
- package/assets/skills/poteto-mode/scripts/orch/orch.test.mjs +2042 -0
- package/assets/skills/poteto-mode/scripts/watch-pr/cli.test.mjs +5101 -0
- package/assets/skills/poteto-mode/scripts/watch-pr/github.test.mjs +1177 -0
- package/assets/skills/poteto-mode/scripts/watch-pr/policy.test.mjs +1441 -0
- package/assets/skills/poteto-mode/scripts/watch-pr/watch-pr.mjs +4601 -0
- package/assets/skills/poteto-mode/scripts/worktree-audit.mjs +675 -0
- package/assets/skills/principle-attack-the-premise/SKILL.md +21 -0
- package/assets/skills/principle-boundary-discipline/SKILL.md +34 -0
- package/assets/skills/principle-build-the-lever/SKILL.md +23 -0
- package/assets/skills/principle-encode-lessons-in-structure/SKILL.md +11 -0
- package/assets/skills/principle-exhaust-the-design-space/SKILL.md +9 -0
- package/assets/skills/principle-experience-first/SKILL.md +19 -0
- package/assets/skills/principle-explain-the-number/SKILL.md +17 -0
- package/assets/skills/principle-fix-root-causes/SKILL.md +21 -0
- package/assets/skills/principle-foundational-thinking/SKILL.md +21 -0
- package/assets/skills/principle-guard-the-context-window/SKILL.md +12 -0
- package/assets/skills/principle-laziness-protocol/SKILL.md +10 -0
- package/assets/skills/principle-make-operations-idempotent/SKILL.md +22 -0
- package/assets/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +14 -0
- package/assets/skills/principle-minimize-reader-load/SKILL.md +18 -0
- package/assets/skills/principle-model-the-domain/SKILL.md +24 -0
- package/assets/skills/principle-never-block-on-the-human/SKILL.md +15 -0
- package/assets/skills/principle-outcome-oriented-execution/SKILL.md +21 -0
- package/assets/skills/principle-prove-it-works/SKILL.md +24 -0
- package/assets/skills/principle-redesign-from-first-principles/SKILL.md +16 -0
- package/assets/skills/principle-separate-before-serializing-shared-state/SKILL.md +16 -0
- package/assets/skills/principle-sequence-verifiable-units/SKILL.md +17 -0
- package/assets/skills/principle-subtract-before-you-add/SKILL.md +18 -0
- package/assets/skills/principle-test-behavior-not-implementation/SKILL.md +20 -0
- package/assets/skills/principle-type-system-discipline/SKILL.md +13 -0
- package/assets/skills/recall/SKILL.md +37 -0
- package/assets/skills/reflect/SKILL.md +69 -0
- package/assets/skills/reflect/references/divergent-reviewer.md +38 -0
- package/assets/skills/reflect/references/judgment-reviewer.md +37 -0
- package/assets/skills/reflect/references/synthesizer.md +56 -0
- package/assets/skills/reflect/references/tooling-reviewer.md +48 -0
- package/assets/skills/setup-pstack/SKILL.md +67 -0
- package/assets/skills/sharpen/SKILL.md +16 -0
- package/assets/skills/show-me-your-work/SKILL.md +84 -0
- package/assets/skills/show-me-your-work/references/decision-log-template.tsv +1 -0
- package/assets/skills/show-me-your-work/scripts/log.mjs +108 -0
- package/assets/skills/swarm/SKILL.md +43 -0
- package/assets/skills/tdd/SKILL.md +42 -0
- package/assets/skills/teach/SKILL.md +23 -0
- package/assets/skills/technical-writing/SKILL.md +108 -0
- package/assets/skills/typescript-best-practices/SKILL.md +34 -0
- package/assets/skills/typescript-best-practices/references/patterns.md +328 -0
- package/assets/skills/unslop/SKILL.md +62 -0
- package/assets/skills/why/SKILL.md +100 -0
- package/assets/skills/why/references/epistemics.md +71 -0
- package/assets/skills/why/references/investigator-prompt.md +84 -0
- package/assets/skills/why/references/source-playbook.md +17 -0
- package/assets/skills/why/references/sources/code-archaeology.md +51 -0
- package/assets/skills/why/references/sources/databricks.md +70 -0
- package/assets/skills/why/references/sources/datadog.md +97 -0
- package/assets/skills/why/references/sources/incident-postmortem.md +5 -0
- package/assets/skills/why/references/sources/linear.md +48 -0
- package/assets/skills/why/references/sources/notion.md +55 -0
- package/assets/skills/why/references/sources/sentry.md +66 -0
- package/assets/skills/why/references/sources/slack.md +17 -0
- package/assets/skills/why/references/synthesizer-prompt.md +129 -0
- package/cordis.patch.yml +39 -0
- package/dist/frontmatter-BQX3ekwP.mjs +229 -0
- package/dist/frontmatter-BQX3ekwP.mjs.map +1 -0
- package/dist/history.d.mts +429 -0
- package/dist/history.mjs +1129 -0
- package/dist/history.mjs.map +1 -0
- package/dist/host-contract-o47JH_r8.d.mts +527 -0
- package/dist/index.d.mts +278 -0
- package/dist/index.mjs +272 -0
- package/dist/index.mjs.map +1 -0
- package/dist/paths-D_JlVmBk.mjs +42 -0
- package/dist/paths-D_JlVmBk.mjs.map +1 -0
- package/dist/script.d.mts +193 -0
- package/dist/script.mjs +782 -0
- package/dist/script.mjs.map +1 -0
- package/dist/subagent.d.mts +336 -0
- package/dist/subagent.mjs +1402 -0
- package/dist/subagent.mjs.map +1 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +106 -3
- package/static/icon.png +0 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
4
|
+
import { isAbsolute, resolve, sep } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
//#region .runtime-sources/skills/poteto-mode/scripts/worktree-audit.ts
|
|
7
|
+
/**
|
|
8
|
+
* Read-only worktree prune audit.
|
|
9
|
+
*
|
|
10
|
+
* A Node replacement for the upstream `scripts/worktree-audit.sh`. The shell
|
|
11
|
+
* script's job was to classify every git worktree by size, age, merge state,
|
|
12
|
+
* uncommitted work, remote/PR state, and the newest supplied session, then print
|
|
13
|
+
* one TSV table sorted by size with a suggested bucket. That job is unchanged; what
|
|
14
|
+
* changed is the toolchain. This file runs under Node alone, needs no `bash`, no
|
|
15
|
+
* `jq`, and no `awk`, and reads the caller's session JSON with `JSON.parse`.
|
|
16
|
+
*
|
|
17
|
+
* Behaviour kept deliberately identical, because the playbook and the tests name
|
|
18
|
+
* the columns:
|
|
19
|
+
*
|
|
20
|
+
* - The header is `# base: <ref>` then one TSV header line, written once.
|
|
21
|
+
* - Columns are SIZE, AGE, MERGED, DIRTY, REMOTE, PR, LAST_CHAT, PINNED, BUCKET,
|
|
22
|
+
* WORKTREE, in that order.
|
|
23
|
+
* - The main worktree is never a candidate. Only `git worktree list` provides
|
|
24
|
+
* paths; nothing is hand-typed.
|
|
25
|
+
* - `MERGED` is `YES` only when the worktree HEAD is an ancestor of the base.
|
|
26
|
+
* Squash merges are not ancestors, so the PR state is the real signal there.
|
|
27
|
+
* - `DIRTY` counts tracked edits as `wip:<n>` and untracked-only as `scratch:<n>`,
|
|
28
|
+
* with `clean` for neither.
|
|
29
|
+
* - `REMOTE` is `pushed`, `ahead<n>`, `detached`, or `no-remote`.
|
|
30
|
+
* - `PR` is `#<number>/<state>` from the forge, or `-`.
|
|
31
|
+
* - `PINNED` is always `unknown`: this host exposes no pinned-state source, so the
|
|
32
|
+
* user's own set decides and the script says so instead of guessing.
|
|
33
|
+
* - `BUCKET` is `hold-wip`, `hold-open-pr`, `unknown-usage`, `verify-recent-chat`,
|
|
34
|
+
* `safe`, or `review`. An unknown usage signal never reaches `safe`.
|
|
35
|
+
* - Rows are sorted by SIZE descending, then kept in the order they were produced.
|
|
36
|
+
* - The session store is never scanned. `--sessions` is the only usage input, and
|
|
37
|
+
* without it the LAST_CHAT column is `-` and the bucket is `unknown-usage`.
|
|
38
|
+
*/
|
|
39
|
+
/** The shape `--sessions` takes, repeated in every message about it. */
|
|
40
|
+
const SESSIONS_SHAPE = "a JSON array of {\"sessionId\",\"cwd\",\"lastEventTime\"} objects";
|
|
41
|
+
const PINNED = "unknown";
|
|
42
|
+
const HEADER = [
|
|
43
|
+
"SIZE",
|
|
44
|
+
"AGE",
|
|
45
|
+
"MERGED",
|
|
46
|
+
"DIRTY",
|
|
47
|
+
"REMOTE",
|
|
48
|
+
"PR",
|
|
49
|
+
"LAST_CHAT",
|
|
50
|
+
"PINNED",
|
|
51
|
+
"BUCKET",
|
|
52
|
+
"WORKTREE"
|
|
53
|
+
];
|
|
54
|
+
/** The bucket for a worktree whose usage signal is missing. */
|
|
55
|
+
const UNKNOWN_USAGE_BUCKET = "unknown-usage";
|
|
56
|
+
/** Raised for a usage problem; the caller prints the message and exits nonzero. */
|
|
57
|
+
var AuditError = class extends Error {
|
|
58
|
+
exitCode;
|
|
59
|
+
constructor(message, exitCode = 1) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.name = "AuditError";
|
|
62
|
+
this.exitCode = exitCode;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
/** How many days a worktree may be untouched and still count as recent. */
|
|
66
|
+
const RECENT_DAYS = 4;
|
|
67
|
+
/**
|
|
68
|
+
* Run git, returning stdout, or `undefined` when it fails.
|
|
69
|
+
*
|
|
70
|
+
* The shell script wrote `2>/dev/null` on most calls and branched on the exit
|
|
71
|
+
* status, so a failure is a normal value here rather than an exception.
|
|
72
|
+
*/
|
|
73
|
+
function git(repo, args) {
|
|
74
|
+
try {
|
|
75
|
+
return execFileSync("git", [
|
|
76
|
+
"-C",
|
|
77
|
+
repo,
|
|
78
|
+
...args
|
|
79
|
+
], {
|
|
80
|
+
encoding: "utf8",
|
|
81
|
+
stdio: [
|
|
82
|
+
"ignore",
|
|
83
|
+
"pipe",
|
|
84
|
+
"ignore"
|
|
85
|
+
],
|
|
86
|
+
maxBuffer: 33554432
|
|
87
|
+
});
|
|
88
|
+
} catch {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Run git without `-C`, for commands that must see the real repository root. */
|
|
93
|
+
function gitHere(cwd, args) {
|
|
94
|
+
try {
|
|
95
|
+
return execFileSync("git", args, {
|
|
96
|
+
cwd,
|
|
97
|
+
encoding: "utf8",
|
|
98
|
+
stdio: [
|
|
99
|
+
"ignore",
|
|
100
|
+
"pipe",
|
|
101
|
+
"ignore"
|
|
102
|
+
],
|
|
103
|
+
maxBuffer: 33554432
|
|
104
|
+
});
|
|
105
|
+
} catch {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** `git worktree list --porcelain`, parsed into worktree paths in order. */
|
|
110
|
+
function worktreePaths(repo) {
|
|
111
|
+
const out = git(repo, [
|
|
112
|
+
"worktree",
|
|
113
|
+
"list",
|
|
114
|
+
"--porcelain"
|
|
115
|
+
]);
|
|
116
|
+
if (out === void 0) return [];
|
|
117
|
+
return out.split("\n").filter((line) => line.startsWith("worktree ")).map((line) => line.slice(9));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Human size, matching `du -h`.
|
|
121
|
+
*
|
|
122
|
+
* GNU `du -h` divides by 1024, promotes the unit when the value reaches 1024, and
|
|
123
|
+
* prints one decimal below ten and a rounded integer at or above ten. Verified
|
|
124
|
+
* against `du` on this host: 8.0K for 4096 bytes, 12K for 8192, 1.1M for 1 MiB,
|
|
125
|
+
* 513M for 512 MiB. Reproducing the format keeps the column comparable with the
|
|
126
|
+
* numbers the playbook quotes and removes the dependency on `du` and `awk`.
|
|
127
|
+
*/
|
|
128
|
+
function formatSize(bytes) {
|
|
129
|
+
const units = [
|
|
130
|
+
"B",
|
|
131
|
+
"K",
|
|
132
|
+
"M",
|
|
133
|
+
"G",
|
|
134
|
+
"T",
|
|
135
|
+
"P"
|
|
136
|
+
];
|
|
137
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
138
|
+
let value = bytes / 1024;
|
|
139
|
+
let unit = 1;
|
|
140
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
141
|
+
value /= 1024;
|
|
142
|
+
unit += 1;
|
|
143
|
+
}
|
|
144
|
+
const suffix = units[unit];
|
|
145
|
+
return `${value < 10 ? value.toFixed(1) : String(Math.round(value))}${suffix}`;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Directory size in bytes as `du -s` counts it: allocated blocks, directories
|
|
149
|
+
* included.
|
|
150
|
+
*
|
|
151
|
+
* Three details make this match `du`, and each was checked against it on this host:
|
|
152
|
+
*
|
|
153
|
+
* - `stat.blocks` is in 512-byte units, not bytes.
|
|
154
|
+
* - A directory consumes blocks of its own, so its `blocks` counts too. Skipping
|
|
155
|
+
* them reads one 4 KiB block low per directory.
|
|
156
|
+
* - `.git` here is a file in a linked worktree, not a directory, so it contributes
|
|
157
|
+
* its own block and nothing is traversed through it.
|
|
158
|
+
*
|
|
159
|
+
* That block accounting is the point of the column: the playbook quotes the space
|
|
160
|
+
* `du` reports before deleting. A symlink contributes its own block and is never
|
|
161
|
+
* traversed, matching `du` without `-L`.
|
|
162
|
+
*/
|
|
163
|
+
function directorySize(root) {
|
|
164
|
+
let total = 0;
|
|
165
|
+
const pending = [root];
|
|
166
|
+
while (pending.length > 0) {
|
|
167
|
+
const current = pending.pop();
|
|
168
|
+
let details;
|
|
169
|
+
try {
|
|
170
|
+
details = statSync(current, { throwIfNoEntry: false });
|
|
171
|
+
} catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (details === void 0) continue;
|
|
175
|
+
total += details.blocks === void 0 ? details.size : details.blocks * 512;
|
|
176
|
+
if (!details.isDirectory()) continue;
|
|
177
|
+
let names;
|
|
178
|
+
try {
|
|
179
|
+
names = readdirSync(current);
|
|
180
|
+
} catch {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
for (const name of names) pending.push(resolve(current, name));
|
|
184
|
+
}
|
|
185
|
+
return total;
|
|
186
|
+
}
|
|
187
|
+
/** `SIZE` as `du -sh` would print it, or `-` when the directory is gone. */
|
|
188
|
+
function sizeColumn(worktree) {
|
|
189
|
+
try {
|
|
190
|
+
if (!statSync(worktree).isDirectory()) return "-";
|
|
191
|
+
} catch {
|
|
192
|
+
return "-";
|
|
193
|
+
}
|
|
194
|
+
return formatSize(directorySize(worktree));
|
|
195
|
+
}
|
|
196
|
+
/** `AGE` as `<n>d`, or `?` when the HEAD time is unavailable. */
|
|
197
|
+
function ageColumn(headTimestamp, now) {
|
|
198
|
+
if (!(headTimestamp > 0)) return "?";
|
|
199
|
+
return `${Math.floor((now - headTimestamp) / 86400)}d`;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* `DIRTY` for one worktree.
|
|
203
|
+
*
|
|
204
|
+
* `git status --porcelain` marks untracked entries with `??`. Anything else is
|
|
205
|
+
* tracked work, which is what a human has to look at before deleting.
|
|
206
|
+
*/
|
|
207
|
+
function dirtyColumn(porcelain) {
|
|
208
|
+
const lines = porcelain.split("\n").filter((line) => line !== "");
|
|
209
|
+
if (lines.length === 0) return "clean";
|
|
210
|
+
const tracked = lines.filter((line) => !line.startsWith("??"));
|
|
211
|
+
if (tracked.length > 0) return `wip:${tracked.length}`;
|
|
212
|
+
return `scratch:${lines.filter((line) => line.startsWith("??")).length}`;
|
|
213
|
+
}
|
|
214
|
+
/** `MERGED` for one worktree, from whether HEAD is an ancestor of the base. */
|
|
215
|
+
function mergedColumn(isAncestor) {
|
|
216
|
+
return isAncestor ? "YES" : "no";
|
|
217
|
+
}
|
|
218
|
+
/** `REMOTE` for one worktree. */
|
|
219
|
+
function remoteColumn(input) {
|
|
220
|
+
if (input.branch === void 0 || input.branch === "") return "detached";
|
|
221
|
+
if (!input.hasRemote) return "no-remote";
|
|
222
|
+
if (input.remoteSha !== void 0 && input.head !== void 0 && input.remoteSha === input.head) return "pushed";
|
|
223
|
+
return `ahead${input.aheadCount ?? 0}`;
|
|
224
|
+
}
|
|
225
|
+
/** `PR` for one branch, matching the first entry the forge reported for it. */
|
|
226
|
+
function prColumn(branch, prs) {
|
|
227
|
+
if (branch === void 0 || branch === "") return "-";
|
|
228
|
+
const hit = prs.find((pr) => pr.head === branch);
|
|
229
|
+
return hit === void 0 ? "-" : `#${hit.number}/${hit.state}`;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Whether a session's recorded cwd is this worktree or a directory below it.
|
|
233
|
+
*
|
|
234
|
+
* Both sides go through `resolve` before comparing. `git worktree list` returns
|
|
235
|
+
* platform separators while a session's `cwd` may have been written by a different
|
|
236
|
+
* source, so on Windows `C:\repo\wt` and `C:/repo/wt` name the same checkout but are
|
|
237
|
+
* different strings. The old comparison hardcoded `/`, so it matched nothing there:
|
|
238
|
+
* every `LAST_CHAT` was `-` and every bucket silently degraded to `unknown-usage`.
|
|
239
|
+
*
|
|
240
|
+
* `resolve` also normalises trailing separators and `..` segments, so neither can
|
|
241
|
+
* make one path look like an ancestor of the other by accident.
|
|
242
|
+
*/
|
|
243
|
+
function sameOrInside(cwd, worktree) {
|
|
244
|
+
const child = resolve(cwd);
|
|
245
|
+
const parent = resolve(worktree);
|
|
246
|
+
return child === parent || child.startsWith(`${parent}${sep}`);
|
|
247
|
+
}
|
|
248
|
+
/** The newest supplied session whose cwd is this worktree or below it. */
|
|
249
|
+
function lastSession(worktree, sessions) {
|
|
250
|
+
if (sessions === void 0 || sessions.length === 0) return void 0;
|
|
251
|
+
const matches = sessions.filter((session) => sameOrInside(session.cwd ?? "", worktree));
|
|
252
|
+
if (matches.length === 0) return void 0;
|
|
253
|
+
return matches.toSorted((left, right) => (left.lastEventTime ?? 0) - (right.lastEventTime ?? 0))[matches.length - 1];
|
|
254
|
+
}
|
|
255
|
+
/** `LAST_CHAT` for one worktree. */
|
|
256
|
+
function lastChatColumn(session) {
|
|
257
|
+
return session === void 0 ? "-" : session.sessionId ?? "unknown";
|
|
258
|
+
}
|
|
259
|
+
/** Whether a session counts as recent for the bucket decision. */
|
|
260
|
+
function isRecent(session, now) {
|
|
261
|
+
const time = session?.lastEventTime ?? 0;
|
|
262
|
+
if (!(time > 0)) return false;
|
|
263
|
+
return Math.floor((now - time) / 86400) <= 4;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* The suggested bucket.
|
|
267
|
+
*
|
|
268
|
+
* Uncommitted tracked work outranks everything, then an open PR, then the usage
|
|
269
|
+
* signal. An unknown usage signal never reaches `safe`: without the caller's
|
|
270
|
+
* session input the bucket stays `unknown-usage`, and the playbook asks the user.
|
|
271
|
+
*/
|
|
272
|
+
function bucketFor(input) {
|
|
273
|
+
if (input.dirty.startsWith("wip:")) return "hold-wip";
|
|
274
|
+
if (input.pr.endsWith("/open") || input.pr.endsWith("/draft")) return "hold-open-pr";
|
|
275
|
+
if (!input.usageKnown) return UNKNOWN_USAGE_BUCKET;
|
|
276
|
+
if (input.usedRecently) return "verify-recent-chat";
|
|
277
|
+
if (input.merged === "YES" || input.pr !== "-") return "safe";
|
|
278
|
+
return "review";
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Read this user's PRs from whichever forge CLI can answer.
|
|
282
|
+
*
|
|
283
|
+
* `origin` reports `status`, `gh` reports `state`; both are normalized to a
|
|
284
|
+
* lowercase `state` here. An unreachable forge is an empty list, not an error: the
|
|
285
|
+
* PR column is one signal among several and a first pass may run offline.
|
|
286
|
+
*/
|
|
287
|
+
function readForgePrs(cwd, run = execFileSync) {
|
|
288
|
+
const origin = tryJson(run, cwd, "origin", [
|
|
289
|
+
"pr",
|
|
290
|
+
"list",
|
|
291
|
+
"--author",
|
|
292
|
+
"@me",
|
|
293
|
+
"--state",
|
|
294
|
+
"all",
|
|
295
|
+
"--limit",
|
|
296
|
+
"1000",
|
|
297
|
+
"--json",
|
|
298
|
+
"number,status,headRef"
|
|
299
|
+
]);
|
|
300
|
+
if (origin !== void 0) return normalizeOrigin(origin);
|
|
301
|
+
const gh = tryJson(run, cwd, "gh", [
|
|
302
|
+
"pr",
|
|
303
|
+
"list",
|
|
304
|
+
"--author",
|
|
305
|
+
"@me",
|
|
306
|
+
"--state",
|
|
307
|
+
"all",
|
|
308
|
+
"--limit",
|
|
309
|
+
"1000",
|
|
310
|
+
"--json",
|
|
311
|
+
"number,state,headRefName"
|
|
312
|
+
]);
|
|
313
|
+
if (gh !== void 0) return normalizeGh(gh);
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
function tryJson(run, cwd, command, args) {
|
|
317
|
+
try {
|
|
318
|
+
const out = run(command, args, {
|
|
319
|
+
cwd,
|
|
320
|
+
encoding: "utf8",
|
|
321
|
+
stdio: [
|
|
322
|
+
"ignore",
|
|
323
|
+
"pipe",
|
|
324
|
+
"ignore"
|
|
325
|
+
],
|
|
326
|
+
maxBuffer: 33554432
|
|
327
|
+
});
|
|
328
|
+
return JSON.parse(out);
|
|
329
|
+
} catch {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/** `[{number, status, headRef}]` from the `origin` CLI. */
|
|
334
|
+
function normalizeOrigin(raw) {
|
|
335
|
+
if (!Array.isArray(raw)) return [];
|
|
336
|
+
const prs = [];
|
|
337
|
+
for (const entry of raw) {
|
|
338
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
339
|
+
const record = entry;
|
|
340
|
+
const number = record["number"];
|
|
341
|
+
const head = record["headRef"];
|
|
342
|
+
if (typeof number !== "number" || typeof head !== "string") continue;
|
|
343
|
+
prs.push({
|
|
344
|
+
number,
|
|
345
|
+
state: String(record["status"] ?? ""),
|
|
346
|
+
head
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return prs;
|
|
350
|
+
}
|
|
351
|
+
/** `[{number, state, headRefName}]` from the `gh` CLI, with a lowercase state. */
|
|
352
|
+
function normalizeGh(raw) {
|
|
353
|
+
if (!Array.isArray(raw)) return [];
|
|
354
|
+
const prs = [];
|
|
355
|
+
for (const entry of raw) {
|
|
356
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
357
|
+
const record = entry;
|
|
358
|
+
const number = record["number"];
|
|
359
|
+
const head = record["headRefName"];
|
|
360
|
+
if (typeof number !== "number" || typeof head !== "string") continue;
|
|
361
|
+
prs.push({
|
|
362
|
+
number,
|
|
363
|
+
state: String(record["state"] ?? "").toLowerCase(),
|
|
364
|
+
head
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return prs;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Pick the base ref when the caller gave no `-b`.
|
|
371
|
+
*
|
|
372
|
+
* `origin/HEAD`, then `origin/main`, then `origin/master`, matching the shell
|
|
373
|
+
* script. An empty result means no base exists and the caller must pass one.
|
|
374
|
+
*/
|
|
375
|
+
function resolveBase(repo) {
|
|
376
|
+
const candidates = [];
|
|
377
|
+
const head = git(repo, [
|
|
378
|
+
"symbolic-ref",
|
|
379
|
+
"--short",
|
|
380
|
+
"refs/remotes/origin/HEAD"
|
|
381
|
+
]);
|
|
382
|
+
if (head !== void 0) candidates.push(head.trim());
|
|
383
|
+
candidates.push("origin/main", "origin/master");
|
|
384
|
+
return candidates.find((candidate) => candidate !== "" && git(repo, [
|
|
385
|
+
"rev-parse",
|
|
386
|
+
"--verify",
|
|
387
|
+
"--quiet",
|
|
388
|
+
`${candidate}^{commit}`
|
|
389
|
+
]) !== void 0);
|
|
390
|
+
}
|
|
391
|
+
/** Build one row. Every column is a pure function so each has a unit test. */
|
|
392
|
+
function buildRow(worktree, context) {
|
|
393
|
+
const head = git(worktree, ["rev-parse", "HEAD"])?.trim();
|
|
394
|
+
const headTimestamp = Number(git(worktree, [
|
|
395
|
+
"log",
|
|
396
|
+
"-1",
|
|
397
|
+
"--format=%ct",
|
|
398
|
+
"HEAD"
|
|
399
|
+
])?.trim() ?? 0);
|
|
400
|
+
const isAncestor = head !== void 0 && head !== "" && head !== "0" ? git(worktree, [
|
|
401
|
+
"merge-base",
|
|
402
|
+
"--is-ancestor",
|
|
403
|
+
head,
|
|
404
|
+
context.base
|
|
405
|
+
]) !== void 0 : false;
|
|
406
|
+
const dirty = dirtyColumn(git(worktree, ["status", "--porcelain"]) ?? "");
|
|
407
|
+
const branchText = git(worktree, [
|
|
408
|
+
"symbolic-ref",
|
|
409
|
+
"--quiet",
|
|
410
|
+
"--short",
|
|
411
|
+
"HEAD"
|
|
412
|
+
]);
|
|
413
|
+
const branch = branchText === void 0 ? "" : branchText.trim();
|
|
414
|
+
const hasRemote = branch !== "" && git(worktree, [
|
|
415
|
+
"show-ref",
|
|
416
|
+
"--verify",
|
|
417
|
+
"--quiet",
|
|
418
|
+
`refs/remotes/origin/${branch}`
|
|
419
|
+
]) !== void 0;
|
|
420
|
+
const remoteSha = hasRemote ? git(worktree, ["rev-parse", `origin/${branch}`])?.trim() : void 0;
|
|
421
|
+
const aheadText = hasRemote ? git(worktree, [
|
|
422
|
+
"rev-list",
|
|
423
|
+
"--count",
|
|
424
|
+
`origin/${branch}..HEAD`
|
|
425
|
+
]) : void 0;
|
|
426
|
+
const remote = remoteColumn({
|
|
427
|
+
branch: branch === "" ? void 0 : branch,
|
|
428
|
+
hasRemote,
|
|
429
|
+
remoteSha,
|
|
430
|
+
head: head?.trim(),
|
|
431
|
+
aheadCount: aheadText === void 0 ? void 0 : Number(aheadText.trim())
|
|
432
|
+
});
|
|
433
|
+
const pr = prColumn(branch === "" ? void 0 : branch, context.prs);
|
|
434
|
+
const session = lastSession(worktree, context.sessions);
|
|
435
|
+
const usageKnown = session !== void 0;
|
|
436
|
+
const sessionsProvided = context.sessions !== void 0;
|
|
437
|
+
const bucket = bucketFor({
|
|
438
|
+
dirty,
|
|
439
|
+
pr,
|
|
440
|
+
merged: mergedColumn(isAncestor),
|
|
441
|
+
usageKnown,
|
|
442
|
+
usedRecently: isRecent(session, context.now)
|
|
443
|
+
});
|
|
444
|
+
return {
|
|
445
|
+
size: sizeColumn(worktree),
|
|
446
|
+
age: ageColumn(headTimestamp, context.now),
|
|
447
|
+
merged: mergedColumn(isAncestor),
|
|
448
|
+
dirty,
|
|
449
|
+
remote,
|
|
450
|
+
pr,
|
|
451
|
+
lastChat: lastChatColumn(session),
|
|
452
|
+
pinned: PINNED,
|
|
453
|
+
bucket,
|
|
454
|
+
note: usageNote({
|
|
455
|
+
usageKnown,
|
|
456
|
+
sessionsProvided
|
|
457
|
+
}),
|
|
458
|
+
worktree
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Why a row's usage signal is what it is.
|
|
463
|
+
*
|
|
464
|
+
* Both "nobody supplied a session list" and "a list was supplied but it never
|
|
465
|
+
* mentioned this worktree" produce the `unknown-usage` bucket, and the caller has
|
|
466
|
+
* to fix a different thing in each case: build the list, or widen it. Collapsing
|
|
467
|
+
* them would lose that distinction, which is the only diagnostic the row carries.
|
|
468
|
+
*/
|
|
469
|
+
function usageNote(input) {
|
|
470
|
+
if (input.usageKnown) return void 0;
|
|
471
|
+
return input.sessionsProvided ? "no session in the supplied list touched this worktree" : "no session list supplied";
|
|
472
|
+
}
|
|
473
|
+
/** Sort rows by SIZE descending, preserving the original order on a tie. */
|
|
474
|
+
function sortRows(rows) {
|
|
475
|
+
const sizeOf = (row) => {
|
|
476
|
+
const match = /^([\d.]+)([BKMGTP]?)$/.exec(row.size);
|
|
477
|
+
if (match === null) return 0;
|
|
478
|
+
return Number(match[1]) * 1024 ** ({
|
|
479
|
+
"": 0,
|
|
480
|
+
B: 0,
|
|
481
|
+
K: 1,
|
|
482
|
+
M: 2,
|
|
483
|
+
G: 3,
|
|
484
|
+
T: 4,
|
|
485
|
+
P: 5
|
|
486
|
+
}[match[2]] ?? 0);
|
|
487
|
+
};
|
|
488
|
+
return rows.map((row, index) => ({
|
|
489
|
+
row,
|
|
490
|
+
index
|
|
491
|
+
})).sort((left, right) => sizeOf(right.row) - sizeOf(left.row) || left.index - right.index).map((entry) => entry.row);
|
|
492
|
+
}
|
|
493
|
+
/** Render the whole table, header included. */
|
|
494
|
+
function renderTable(rows, base) {
|
|
495
|
+
const lines = [`# base: ${base}`, HEADER.join(" ")];
|
|
496
|
+
for (const row of rows) lines.push([
|
|
497
|
+
row.size,
|
|
498
|
+
row.age,
|
|
499
|
+
row.merged,
|
|
500
|
+
row.dirty,
|
|
501
|
+
row.remote,
|
|
502
|
+
row.pr,
|
|
503
|
+
row.lastChat,
|
|
504
|
+
row.pinned,
|
|
505
|
+
row.bucket,
|
|
506
|
+
row.worktree
|
|
507
|
+
].join(" "));
|
|
508
|
+
const notes = rows.filter((row) => row.note !== void 0);
|
|
509
|
+
if (notes.length > 0) {
|
|
510
|
+
lines.push("# usage signal unknown for:");
|
|
511
|
+
for (const row of notes) lines.push(`# ${row.worktree}: ${row.note}`);
|
|
512
|
+
}
|
|
513
|
+
return `${lines.join("\n")}\n`;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Read and validate the `--sessions` file.
|
|
517
|
+
*
|
|
518
|
+
* The shell version required a JSON array via `jq -e 'type == "array"'` and pinned
|
|
519
|
+
* the path before changing directory. Both hold here: the path is resolved before
|
|
520
|
+
* any `git -C`, and a non-array is an error rather than an empty list, because a
|
|
521
|
+
* silently ignored usage signal would quietly turn every bucket into
|
|
522
|
+
* `unknown-usage`.
|
|
523
|
+
*
|
|
524
|
+
* Every element is checked, not filtered. The array is a projection the caller
|
|
525
|
+
* builds from `pstack_history` results, so a malformed entry is a real mistake; the
|
|
526
|
+
* previous `filter` dropped such entries and let the row fall through to
|
|
527
|
+
* `unknown-usage`, which is exactly the silent data loss the paragraph above says
|
|
528
|
+
* this function refuses to do.
|
|
529
|
+
*/
|
|
530
|
+
function readSessionsFile(path) {
|
|
531
|
+
const absolute = isAbsolute(path) ? path : resolve(process.cwd(), path);
|
|
532
|
+
let text;
|
|
533
|
+
try {
|
|
534
|
+
text = readFileSync(absolute, "utf8");
|
|
535
|
+
} catch {
|
|
536
|
+
throw new AuditError(`cannot read sessions file: ${path}`);
|
|
537
|
+
}
|
|
538
|
+
let parsed;
|
|
539
|
+
try {
|
|
540
|
+
parsed = JSON.parse(text);
|
|
541
|
+
} catch {
|
|
542
|
+
throw new AuditError(`sessions file must be ${SESSIONS_SHAPE}: ${path}`);
|
|
543
|
+
}
|
|
544
|
+
if (!Array.isArray(parsed)) throw new AuditError(`sessions file must be ${SESSIONS_SHAPE}: ${path}. Build it from pstack_history results; the history page itself is not an array.`);
|
|
545
|
+
return parsed.map((entry, index) => {
|
|
546
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) throw new AuditError(`sessions[${index}] must be an object, got ${JSON.stringify(entry)?.slice(0, 60) ?? "undefined"}`);
|
|
547
|
+
const { sessionId, cwd, lastEventTime } = entry;
|
|
548
|
+
if (typeof sessionId !== "string" || sessionId === "") throw new AuditError(`sessions[${index}].sessionId must be a non-empty string`);
|
|
549
|
+
if (cwd !== void 0 && cwd !== null && typeof cwd !== "string") throw new AuditError(`sessions[${index}].cwd must be a string or null`);
|
|
550
|
+
if (lastEventTime !== void 0 && typeof lastEventTime !== "number") throw new AuditError(`sessions[${index}].lastEventTime must be a number`);
|
|
551
|
+
return {
|
|
552
|
+
sessionId,
|
|
553
|
+
...cwd === void 0 ? {} : { cwd },
|
|
554
|
+
...lastEventTime === void 0 ? {} : { lastEventTime }
|
|
555
|
+
};
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
/** Usage text, kept close to the shell script's help. */
|
|
559
|
+
const USAGE = [
|
|
560
|
+
"Usage: worktree-audit.mjs [-b|--base <ref>] [-s|--sessions <json>] [repo-path]",
|
|
561
|
+
" repo-path defaults to the current repo.",
|
|
562
|
+
" -b sets the ref the MERGED column checks against (e.g. origin/integration).",
|
|
563
|
+
" Without it the base is origin/HEAD, then origin/main, then origin/master.",
|
|
564
|
+
" -s takes the JSON you produced with `pstack_history`, an array of",
|
|
565
|
+
" {\"sessionId\",\"cwd\",\"lastEventTime\"}. This script never reads the session",
|
|
566
|
+
" store, so without it LAST_CHAT is \"-\" and the bucket is unknown-usage.",
|
|
567
|
+
" PINNED is always \"unknown\": this host exposes no pinned-state source, so the",
|
|
568
|
+
" user's own set decides.",
|
|
569
|
+
""
|
|
570
|
+
].join("\n");
|
|
571
|
+
/**
|
|
572
|
+
* Parse the command line.
|
|
573
|
+
*
|
|
574
|
+
* An unknown option and a missing argument value both exit 2, as the shell script
|
|
575
|
+
* did; an unreadable sessions file and a missing base exit 1.
|
|
576
|
+
*/
|
|
577
|
+
function parseArgs(argv) {
|
|
578
|
+
let base;
|
|
579
|
+
let sessions;
|
|
580
|
+
const positional = [];
|
|
581
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
582
|
+
const argument = argv[index];
|
|
583
|
+
if (argument === "-h" || argument === "--help") return { help: true };
|
|
584
|
+
if (argument === "-b" || argument === "--base") {
|
|
585
|
+
const value = argv[index + 1];
|
|
586
|
+
if (value === void 0) throw new AuditError(`${argument} needs a ref`, 2);
|
|
587
|
+
base = value;
|
|
588
|
+
index += 1;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (argument === "-s" || argument === "--sessions") {
|
|
592
|
+
const value = argv[index + 1];
|
|
593
|
+
if (value === void 0) throw new AuditError(`${argument} needs a JSON file`, 2);
|
|
594
|
+
sessions = value;
|
|
595
|
+
index += 1;
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (argument.startsWith("-")) throw new AuditError(`unknown option: ${argument}`, 2);
|
|
599
|
+
positional.push(argument);
|
|
600
|
+
}
|
|
601
|
+
if (positional.length > 1) throw new AuditError(`expected at most one repo path, received ${positional.length}`, 2);
|
|
602
|
+
return {
|
|
603
|
+
...base === void 0 ? {} : { base },
|
|
604
|
+
...sessions === void 0 ? {} : { sessions },
|
|
605
|
+
...positional[0] === void 0 ? {} : { repo: positional[0] },
|
|
606
|
+
help: false
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
/** Run the audit. Returns the process exit code. */
|
|
610
|
+
function main(argv, io = {
|
|
611
|
+
stdout: (value) => process.stdout.write(value),
|
|
612
|
+
stderr: (value) => process.stderr.write(value)
|
|
613
|
+
}, options = {}) {
|
|
614
|
+
try {
|
|
615
|
+
const args = parseArgs(argv);
|
|
616
|
+
if (args.help) {
|
|
617
|
+
io.stdout(USAGE);
|
|
618
|
+
return 0;
|
|
619
|
+
}
|
|
620
|
+
const sessions = args.sessions === void 0 ? void 0 : readSessionsFile(args.sessions);
|
|
621
|
+
const cwd = options.cwd ?? process.cwd();
|
|
622
|
+
const repo = args.repo ?? gitHere(cwd, ["rev-parse", "--show-toplevel"])?.trim() ?? (() => {
|
|
623
|
+
throw new AuditError("not in a git repo; pass a repo path");
|
|
624
|
+
})();
|
|
625
|
+
if (git(repo, ["rev-parse", "--git-dir"]) === void 0) throw new AuditError(`not a git repo: ${repo}`);
|
|
626
|
+
const paths = worktreePaths(repo);
|
|
627
|
+
const mainWorktree = paths[0];
|
|
628
|
+
if (mainWorktree === void 0) throw new AuditError(`not a git repo: ${repo}`);
|
|
629
|
+
let base = args.base;
|
|
630
|
+
if (base === void 0) {
|
|
631
|
+
base = resolveBase(repo);
|
|
632
|
+
if (base === void 0) throw new AuditError("no base ref (origin/HEAD, origin/main, origin/master all missing); pass -b <ref>");
|
|
633
|
+
io.stderr(`warn: no -b given; MERGED checks against ${base}\n`);
|
|
634
|
+
}
|
|
635
|
+
if (base.startsWith("origin/")) {
|
|
636
|
+
if (git(repo, [
|
|
637
|
+
"fetch",
|
|
638
|
+
"origin",
|
|
639
|
+
`+refs/heads/${base.slice(7)}:refs/remotes/${base}`,
|
|
640
|
+
"--quiet"
|
|
641
|
+
]) === void 0) io.stderr(`warn: could not fetch ${base}; merged column may be stale\n`);
|
|
642
|
+
}
|
|
643
|
+
if (git(repo, [
|
|
644
|
+
"rev-parse",
|
|
645
|
+
"--verify",
|
|
646
|
+
"--quiet",
|
|
647
|
+
`${base}^{commit}`
|
|
648
|
+
]) === void 0) throw new AuditError(`base ref not found: ${base}`);
|
|
649
|
+
const prs = readForgePrs(repo);
|
|
650
|
+
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
651
|
+
const context = {
|
|
652
|
+
base,
|
|
653
|
+
repo,
|
|
654
|
+
mainWorktree,
|
|
655
|
+
prs,
|
|
656
|
+
sessions,
|
|
657
|
+
now
|
|
658
|
+
};
|
|
659
|
+
const rows = paths.filter((path) => path !== mainWorktree).map((path) => buildRow(path, context));
|
|
660
|
+
io.stdout(renderTable(sortRows(rows), base));
|
|
661
|
+
return 0;
|
|
662
|
+
} catch (error) {
|
|
663
|
+
if (error instanceof AuditError) {
|
|
664
|
+
io.stderr(`error: ${error.message}\n`);
|
|
665
|
+
return error.exitCode;
|
|
666
|
+
}
|
|
667
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
668
|
+
io.stderr(`error: ${message}\n`);
|
|
669
|
+
return 1;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const entryPoint = process.argv[1];
|
|
673
|
+
if (entryPoint !== void 0 && resolve(entryPoint) === fileURLToPath(import.meta.url)) process.exitCode = main(process.argv.slice(2));
|
|
674
|
+
//#endregion
|
|
675
|
+
export { AuditError, HEADER, PINNED, RECENT_DAYS, UNKNOWN_USAGE_BUCKET, USAGE, ageColumn, bucketFor, buildRow, dirtyColumn, formatSize, isRecent, lastChatColumn, lastSession, main, mergedColumn, normalizeGh, normalizeOrigin, parseArgs, prColumn, readForgePrs, readSessionsFile, remoteColumn, renderTable, resolveBase, sameOrInside, sizeColumn, sortRows, usageNote, worktreePaths };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: principle-attack-the-premise
|
|
3
|
+
description: Apply when two or more fixes that share one premise have failed the same gate. Write the premise down and test it directly before you write another fix that assumes it.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Attack the Premise
|
|
8
|
+
|
|
9
|
+
When two or more fixes that share one premise have failed the same gate, suspect the premise, not the fixes.
|
|
10
|
+
|
|
11
|
+
**Why:** Each failed fix is evidence against the belief it assumed. A third fix on the same premise spends effort where the evidence already says the cause is not.
|
|
12
|
+
|
|
13
|
+
**Pattern:**
|
|
14
|
+
- **Write the premise down.** The premise is the one sentence that every failed fix assumed.
|
|
15
|
+
- **Test the premise before the next fix.** Find an observation that would come out differently if the premise were false, and make it. Look at how the failures are distributed, not only at how large they are. Make the check rerunnable per [Build the Lever](../principle-build-the-lever/SKILL.md).
|
|
16
|
+
- **Follow the evidence.** If it contradicts the premise, the cause is elsewhere. Look for what the failures have in common that the premise did not predict. If it confirms the premise, ask the next "why" per [Fix Root Causes](../principle-fix-root-causes/SKILL.md). The earlier fixes may have treated a consequence instead of the cause.
|
|
17
|
+
- **Remove the condition instead of compensating for it**, per the [Laziness Protocol](../principle-laziness-protocol/SKILL.md). If the evidence shows that one part always lands on the losing side, change what puts it there. A mechanism that cleans up after it leaves the cause in place and adds work on every run.
|
|
18
|
+
|
|
19
|
+
**Stop:** Do not start the next fix before the premise is written down and tested.
|
|
20
|
+
|
|
21
|
+
This principle is distinct from [Redesign from First Principles](../principle-redesign-from-first-principles/SKILL.md), which rebuilds a design around a new requirement. It questions a fact the current design assumes.
|