@massa-ai/codex-plugin 1.55.0 → 1.57.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.
Files changed (66) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/install.sh +3 -3
  3. package/package.json +1 -1
  4. package/skills/agents/architecture-specialist/SKILL.md +1 -1
  5. package/skills/agents/audit-specialist/SKILL.md +1 -1
  6. package/skills/agents/builder/SKILL.md +1 -1
  7. package/skills/agents/context-curator/SKILL.md +1 -1
  8. package/skills/agents/designer/SKILL.md +1 -1
  9. package/skills/agents/documentation-agent/SKILL.md +1 -1
  10. package/skills/agents/furps-analyst/SKILL.md +1 -1
  11. package/skills/agents/investigator/SKILL.md +1 -1
  12. package/skills/agents/judge/SKILL.md +1 -1
  13. package/skills/agents/meta-judge/SKILL.md +1 -1
  14. package/skills/agents/mobile-specialist/SKILL.md +1 -1
  15. package/skills/agents/navigator/SKILL.md +1 -1
  16. package/skills/agents/plan-critic/SKILL.md +1 -1
  17. package/skills/agents/planner/SKILL.md +1 -1
  18. package/skills/agents/requirements-analyst/SKILL.md +1 -1
  19. package/skills/agents/reviewer/SKILL.md +1 -1
  20. package/skills/agents/test-engineer/SKILL.md +1 -1
  21. package/skills/agents/verification-agent/SKILL.md +1 -1
  22. package/skills/bootstrap/SKILL.md +74 -0
  23. package/skills/massa-ai/SKILL.md +2 -1
  24. package/skills/massa-ai/references/agent-orchestration.md +41 -4
  25. package/skills/massa-ai/references/audit-scope.md +1 -1
  26. package/skills/massa-ai/references/code-annotation.md +16 -1
  27. package/skills/massa-ai/references/code-quality-lens.md +107 -0
  28. package/skills/massa-ai/references/code-reuse-scan.md +0 -1
  29. package/skills/massa-ai/references/implementation-delivery.md +6 -3
  30. package/skills/massa-ai/references/naming-standards.md +12 -7
  31. package/skills/massa-ai/references/skill-architect/authoring-principles.md +37 -0
  32. package/skills/massa-ai/references/verification-ladder.md +2 -0
  33. package/skills/massa-ai/scripts/check_commit.ts +4 -1
  34. package/skills/massa-ai/scripts/ensure_worktree.ts +311 -0
  35. package/skills/massa-ai/scripts/resolve_scope.ts +263 -0
  36. package/skills/massa-ai/scripts/size_change.ts +227 -0
  37. package/skills/massa-ai/workflows/architecture/architecture-audit.md +1 -15
  38. package/skills/massa-ai/workflows/architecture/architecture-fix.md +1 -8
  39. package/skills/massa-ai/workflows/bugs/bugs-audit.md +1 -13
  40. package/skills/massa-ai/workflows/bugs/bugs-fix.md +1 -9
  41. package/skills/massa-ai/workflows/code-quality/code-quality-audit.md +3 -34
  42. package/skills/massa-ai/workflows/code-quality/code-quality-fix.md +3 -17
  43. package/skills/massa-ai/workflows/commit.md +6 -6
  44. package/skills/massa-ai/workflows/debug.md +2 -9
  45. package/skills/massa-ai/workflows/design.md +0 -6
  46. package/skills/massa-ai/workflows/exploration.md +0 -1
  47. package/skills/massa-ai/workflows/feature.md +2 -15
  48. package/skills/massa-ai/workflows/general.md +1 -16
  49. package/skills/massa-ai/workflows/implementation/implementation-audit.md +0 -1
  50. package/skills/massa-ai/workflows/implementation/implementation-fix.md +1 -15
  51. package/skills/massa-ai/workflows/judge-with-debate.md +0 -2
  52. package/skills/massa-ai/workflows/maestro/maestro-fix.md +1 -8
  53. package/skills/massa-ai/workflows/mobile-figma/mobile-figma-audit.md +0 -6
  54. package/skills/massa-ai/workflows/mobile-figma/mobile-figma-fix.md +1 -14
  55. package/skills/massa-ai/workflows/pr-review.md +0 -3
  56. package/skills/massa-ai/workflows/refactor.md +2 -9
  57. package/skills/massa-ai/workflows/refinement/furps-refinement.md +0 -1
  58. package/skills/massa-ai/workflows/requirements/requirements-audit.md +1 -13
  59. package/skills/massa-ai/workflows/requirements/requirements-fix.md +1 -8
  60. package/skills/massa-ai/workflows/security/security-audit.md +1 -13
  61. package/skills/massa-ai/workflows/security/security-fix.md +1 -9
  62. package/skills/massa-ai/workflows/skill-architect.md +7 -39
  63. package/skills/massa-ai/workflows/spec-driven.md +0 -11
  64. package/skills/massa-ai/workflows/tests/tests-audit.md +1 -13
  65. package/skills/massa-ai/workflows/tests/tests-fix.md +1 -8
  66. package/skills/massa-ai/workflows/to-prd.md +0 -1
@@ -0,0 +1,37 @@
1
+ # Skill Authoring Principles
2
+
3
+ Read once, before Phase 1 of `workflows/skill-architect.md`. This file carries
4
+ the principles and the conversational manner; the workflow carries the five
5
+ phases, their exit criteria, and the hard rules.
6
+
7
+ ## Core Philosophy
8
+
9
+ 1. **Understand before building.** Never generate a SKILL.md until you've completed Discovery and Architecture phases. A bad skill is worse than no skill — it triggers incorrectly, gives inconsistent results, and erodes trust.
10
+
11
+ 2. **Progressive disclosure is everything.** The three-level system (frontmatter → SKILL.md body → linked files) exists for a reason: token economy. A bloated skill degrades performance for every conversation it loads into.
12
+
13
+ 3. **Composability over completeness.** Skills coexist with other skills. Never assume yours is the only one loaded. Be a good neighbor.
14
+
15
+ 4. **Specificity beats verbosity.** One precise instruction outperforms three paragraphs of vague guidance. Code beats prose for deterministic checks.
16
+
17
+ 5. **Skills are for agents, not humans.** No README.md inside the skill folder. No onboarding documentation. Write for an LLM that needs clear, actionable instructions.
18
+
19
+ ## Phase Sequence
20
+
21
+ ```
22
+ DISCOVERY → ARCHITECTURE → CRAFT → VALIDATE → DELIVER
23
+ ```
24
+
25
+ Move through phases sequentially. Never skip Discovery. Each phase has
26
+ explicit exit criteria before you advance.
27
+
28
+ ## Conversation Style
29
+
30
+ - Ask questions one area at a time — don't dump all Discovery questions at once
31
+ - Give concrete suggestions the user can react to ("Would something like X work?")
32
+ - If the user provides a vague request, propose a specific interpretation and ask
33
+ if it matches their intent
34
+ - If the conversation already contains a workflow (user says "turn this into a
35
+ skill"), extract what you can from history FIRST, then fill gaps with questions
36
+ - Match the user's technical level — explain terms if they seem non-technical
37
+ - Be direct about tradeoffs: if a design choice has a downside, say so
@@ -10,6 +10,8 @@ Classify implementation size before editing:
10
10
  - Standard: <=10 files or <=500 changed LOC within one ownership area, or shared behavior/public API/test impact that still has clear acceptance criteria and no unresolved architecture decision.
11
11
  - Spec-driven: >10 files, >500 changed LOC, multiple ownership areas, unclear acceptance criteria, new dependency, migration, irreversible operation, security/privacy/auth, public compatibility, cross-service contract, or any unresolved architecture/product decision.
12
12
 
13
+ Get the file and LOC halves from `bun skills/massa-ai/scripts/size_change.ts` (`--staged` for the index, `--range <a>..<b>` for a range) rather than counting a diff by eye. It reports changed files, changed LOC (added + deleted), and the resulting **size floor**. The qualitative half of each bullet above — acceptance criteria, dependencies, migrations, irreversible operations, security/privacy/auth, public compatibility, cross-service contracts, unresolved decisions — is invisible to a diff, so the script's answer is a lower bound: raise it when any of those applies, never lower it.
14
+
13
15
  Quick tasks can proceed inside the active workflow. Standard tasks need an explicit verification recipe before edits. Spec-driven tasks should route to `workflows/spec-driven.md` or be split into atomic tasks.
14
16
 
15
17
  ## Shared Reference Trigger Table
@@ -33,7 +33,10 @@
33
33
 
34
34
  import { readFileSync } from "node:fs";
35
35
 
36
- const TYPES = ["feat", "fix", "refactor", "docs", "test", "style", "perf", "build", "ci", "chore"];
36
+ // `revert` is in the list because `workflows/commit.md` §8 names it in the type
37
+ // precedence order the workflow tells the agent to pick from. Without it the
38
+ // script rejected a message the workflow had just instructed the agent to write.
39
+ const TYPES = ["feat", "fix", "refactor", "docs", "test", "style", "perf", "build", "ci", "chore", "revert"];
37
40
 
38
41
  // massa-ai patch (D1c): an optional leading Jira-style key prefix, e.g.
39
42
  // "[SA-142] feat(auth): reject expired tokens" (workflows/commit.md §8). The
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * ensure_worktree.ts - Stage 0-1 of the Implementation Delivery Protocol.
4
+ *
5
+ * `references/implementation-delivery.md` Stage 0 records which capabilities are
6
+ * present, and Stage 1 fetches the base and creates a worktree on a new branch
7
+ * before the first repository mutation. This runs both and prints the worktree
8
+ * path and branch — the isolation evidence Stage 1 requires be recorded the
9
+ * moment it completes. Bun builtins only, zero dependencies, agent-agnostic.
10
+ *
11
+ * This is the HOW, not the WHEN. The Isolation Gate line in each of the 16
12
+ * implementation workflows is what decides that isolation is due; it is
13
+ * byte-identical across those files and gated by
14
+ * `scripts/__tests__/workflow-harness-contract.test.ts`, and nothing here
15
+ * replaces it.
16
+ *
17
+ * The two legal skips are the reference's, quoted verbatim, and the script
18
+ * refuses to invent a third:
19
+ * 1. The target is not a git repository.
20
+ * 2. The user explicitly declined isolation for this task.
21
+ * Only the first is detectable from the tree, and only that one is emitted
22
+ * automatically. The second must be passed with --skip-declined, because a
23
+ * script cannot observe a user declining anything.
24
+ *
25
+ * Failure handling mirrors the Stage 1 table: a taken branch name is suffixed
26
+ * `-2`, `-3`, ...; a taken worktree path is reused ONLY when its branch already
27
+ * matches, and is otherwise an error rather than a silent reuse.
28
+ *
29
+ * Usage:
30
+ * bun skills/massa-ai/scripts/ensure_worktree.ts --branch feat/login-retry
31
+ * bun skills/massa-ai/scripts/ensure_worktree.ts --branch fix/x --base main --path ../wt-fix-x
32
+ * bun skills/massa-ai/scripts/ensure_worktree.ts --skip-declined
33
+ * bun skills/massa-ai/scripts/ensure_worktree.ts --branch feat/x --dry-run
34
+ *
35
+ * Exit codes: 0 isolated or legally skipped, 1 isolation failed, 2 usage error.
36
+ */
37
+
38
+ import path from "node:path";
39
+ import { realpathSync } from "node:fs";
40
+
41
+ const USAGE =
42
+ "usage: ensure_worktree.ts [-h] (--branch BRANCH | --skip-declined) [--base REF] [--path DIR] [--root ROOT] [--dry-run] [--json]";
43
+ const HELP = `${USAGE}
44
+
45
+ Run Stage 0-1 of references/implementation-delivery.md and print the isolation evidence.
46
+
47
+ options:
48
+ -h, --help show this help message and exit
49
+ --branch BRANCH branch to create, e.g. feat/login-retry
50
+ --base REF base ref to branch from (default: origin/HEAD, then main)
51
+ --path DIR worktree directory (default: ../<repo>-<branch slug> beside the repo)
52
+ --root ROOT run git in this directory (default: cwd)
53
+ --skip-declined record the "user explicitly declined isolation" skip and exit 0
54
+ --dry-run print the commands that would run, change nothing
55
+ --json emit a JSON object instead of the text report`;
56
+
57
+ interface Args {
58
+ branch: string | null;
59
+ base: string | null;
60
+ worktreePath: string | null;
61
+ root: string;
62
+ skipDeclined: boolean;
63
+ dryRun: boolean;
64
+ json: boolean;
65
+ }
66
+
67
+ interface Result {
68
+ isolated: boolean;
69
+ worktreePath: string | null;
70
+ branch: string | null;
71
+ base: string | null;
72
+ skipReason: string | null;
73
+ capabilities: { gitRepo: boolean; gh: boolean; ghAuthenticated: boolean };
74
+ commands: string[];
75
+ }
76
+
77
+ /** The reference's two legal skip reasons, verbatim. Any third is a protocol violation. */
78
+ const SKIP_NOT_A_REPO = "The target is not a git repository.";
79
+ const SKIP_USER_DECLINED = "The user explicitly declined isolation for this task.";
80
+
81
+ function printUsageError(msg: string): void {
82
+ process.stderr.write(`${USAGE}\nensure_worktree.ts: error: ${msg}\n`);
83
+ }
84
+
85
+ function parseArgs(argv: string[]): Args | null {
86
+ const args: Args = {
87
+ branch: null, base: null, worktreePath: null, root: ".",
88
+ skipDeclined: false, dryRun: false, json: false,
89
+ };
90
+ const valued: Record<string, keyof Args> = {
91
+ "--branch": "branch", "--base": "base", "--path": "worktreePath", "--root": "root",
92
+ };
93
+ for (let i = 0; i < argv.length; i++) {
94
+ const a = argv[i]!;
95
+ if (a === "-h" || a === "--help") {
96
+ console.log(HELP);
97
+ process.exit(0);
98
+ }
99
+ if (a === "--skip-declined") { args.skipDeclined = true; continue; }
100
+ if (a === "--dry-run") { args.dryRun = true; continue; }
101
+ if (a === "--json") { args.json = true; continue; }
102
+ const eq = a.indexOf("=");
103
+ const flag = eq === -1 ? a : a.slice(0, eq);
104
+ const key = valued[flag];
105
+ if (key === undefined) {
106
+ printUsageError(`unrecognized argument: ${a}`);
107
+ return null;
108
+ }
109
+ let value: string;
110
+ if (eq !== -1) value = a.slice(eq + 1);
111
+ else {
112
+ if (i + 1 >= argv.length) {
113
+ printUsageError(`argument ${flag}: expected one argument`);
114
+ return null;
115
+ }
116
+ value = argv[++i]!;
117
+ }
118
+ (args as Record<string, unknown>)[key] = value;
119
+ }
120
+ if (!args.skipDeclined && args.branch === null) {
121
+ printUsageError("one of --branch or --skip-declined is required");
122
+ return null;
123
+ }
124
+ return args;
125
+ }
126
+
127
+ interface Run { ok: boolean; stdout: string; stderr: string }
128
+
129
+ function run(root: string, cmd: string[]): Run {
130
+ const proc = Bun.spawnSync(cmd, { cwd: root, stdout: "pipe", stderr: "pipe" });
131
+ const dec = new TextDecoder();
132
+ return { ok: proc.exitCode === 0, stdout: dec.decode(proc.stdout), stderr: dec.decode(proc.stderr) };
133
+ }
134
+
135
+ function git(root: string, gitArgs: string[]): Run {
136
+ return run(root, ["git", ...gitArgs]);
137
+ }
138
+
139
+ /** See resolve_scope.ts's defaultBase — same rule, same reason not to invent one. */
140
+ function defaultBase(root: string): string | null {
141
+ const symref = git(root, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"]);
142
+ if (symref.ok) return symref.stdout.trim().replace(/^refs\/remotes\//, "");
143
+ for (const candidate of ["origin/main", "origin/master", "main", "master"]) {
144
+ if (git(root, ["rev-parse", "--verify", "--quiet", candidate]).ok) return candidate;
145
+ }
146
+ return null;
147
+ }
148
+
149
+ /** `feat/login-retry` -> `login-retry`, for the default worktree directory name. */
150
+ function slug(branch: string): string {
151
+ return branch.replace(/[^A-Za-z0-9._/-]/g, "-").split("/").filter(Boolean).join("-");
152
+ }
153
+
154
+ /** First free branch name: `name`, then `name-2`, `name-3`, ... per the Stage 1 table. */
155
+ function freeBranch(root: string, wanted: string): string {
156
+ if (!git(root, ["rev-parse", "--verify", "--quiet", `refs/heads/${wanted}`]).ok) return wanted;
157
+ for (let n = 2; n < 100; n++) {
158
+ const candidate = `${wanted}-${n}`;
159
+ if (!git(root, ["rev-parse", "--verify", "--quiet", `refs/heads/${candidate}`]).ok) return candidate;
160
+ }
161
+ return `${wanted}-${Date.now()}`;
162
+ }
163
+
164
+ /**
165
+ * Canonical form for comparing two paths that may differ only by a symlink.
166
+ *
167
+ * On macOS the system temp dir is `/var/folders/...`, a symlink to
168
+ * `/private/var/folders/...`, and `git worktree list` reports the resolved
169
+ * form while an argument-derived path keeps the unresolved one. `path.resolve`
170
+ * does not resolve symlinks, so comparing with it alone reports "no worktree
171
+ * here" for a worktree that is very much here.
172
+ */
173
+ function canonical(p: string): string {
174
+ try {
175
+ return realpathSync(p);
176
+ } catch {
177
+ return path.resolve(p);
178
+ }
179
+ }
180
+
181
+ /** The branch an existing worktree at `dir` is on, or null when there is none. */
182
+ function worktreeBranchAt(root: string, dir: string): string | null {
183
+ const out = git(root, ["worktree", "list", "--porcelain"]);
184
+ if (!out.ok) return null;
185
+ const want = canonical(path.resolve(root, dir));
186
+ let current: string | null = null;
187
+ for (const line of out.stdout.split("\n")) {
188
+ if (line.startsWith("worktree ")) current = line.slice("worktree ".length).trim();
189
+ else if (line.startsWith("branch ") && current !== null && canonical(current) === want) {
190
+ return line.slice("branch ".length).trim().replace(/^refs\/heads\//, "");
191
+ }
192
+ }
193
+ return null;
194
+ }
195
+
196
+ function ensure(args: Args): Result {
197
+ const commands: string[] = [];
198
+ const isRepo = git(args.root, ["rev-parse", "--is-inside-work-tree"]).ok;
199
+ // `Bun.which`, never a spawned `command -v`: `command` is a shell builtin that
200
+ // macOS also ships as /usr/bin/command and Ubuntu does not, so spawning it
201
+ // succeeds on one CI runner and kills the process with ENOENT on the other.
202
+ const gh = Bun.which("gh") !== null;
203
+ const ghAuthenticated = gh && run(args.root, ["gh", "auth", "status"]).ok;
204
+ const capabilities = { gitRepo: isRepo, gh, ghAuthenticated };
205
+
206
+ if (!isRepo) {
207
+ return { isolated: false, worktreePath: null, branch: null, base: null, skipReason: SKIP_NOT_A_REPO, capabilities, commands };
208
+ }
209
+ if (args.skipDeclined) {
210
+ return { isolated: false, worktreePath: null, branch: null, base: null, skipReason: SKIP_USER_DECLINED, capabilities, commands };
211
+ }
212
+
213
+ const base = args.base ?? defaultBase(args.root);
214
+ if (base === null) {
215
+ throw new Error("no base ref could be resolved and none was given. Pass --base; do not invent a base.");
216
+ }
217
+ const wanted = args.branch!;
218
+ const repoName = path.basename(path.resolve(args.root));
219
+ const dir = args.worktreePath ?? path.join("..", `${repoName}-${slug(wanted)}`);
220
+
221
+ // Resolve the existing-worktree question BEFORE bumping the branch name.
222
+ // Bumping first turns the reuse case into a mismatch against a name the
223
+ // caller never asked for: the path is on `feat/x`, the bumped request is
224
+ // `feat/x-2`, and a legal reuse reads as a collision.
225
+ const existingBranch = worktreeBranchAt(args.root, dir);
226
+ if (existingBranch !== null && !args.dryRun) {
227
+ // Stage 1: "Worktree path taken -> reuse it only if its branch matches."
228
+ if (existingBranch !== wanted) {
229
+ throw new Error(
230
+ `worktree path ${dir} already exists on branch '${existingBranch}', not '${wanted}'. Reuse is legal only when the branch matches.`,
231
+ );
232
+ }
233
+ return {
234
+ isolated: true,
235
+ worktreePath: path.resolve(args.root, dir),
236
+ branch: existingBranch,
237
+ base,
238
+ skipReason: null,
239
+ capabilities,
240
+ commands: [`# reused existing worktree at ${dir} on ${existingBranch}`],
241
+ };
242
+ }
243
+
244
+ const branch = freeBranch(args.root, wanted);
245
+
246
+ // `git fetch origin <base>` only makes sense for a remote-tracking base; a
247
+ // purely local base has nothing to fetch and the fetch would fail noisily.
248
+ const remoteBase = base.startsWith("origin/") ? base.slice("origin/".length) : null;
249
+ if (remoteBase !== null) commands.push(`git fetch origin ${remoteBase}`);
250
+ commands.push(`git worktree add -b ${branch} ${dir} ${base}`);
251
+
252
+ if (args.dryRun) {
253
+ return { isolated: false, worktreePath: path.resolve(args.root, dir), branch, base, skipReason: null, capabilities, commands };
254
+ }
255
+
256
+ if (remoteBase !== null) {
257
+ const fetched = git(args.root, ["fetch", "origin", remoteBase]);
258
+ if (!fetched.ok) throw new Error(`git fetch origin ${remoteBase} failed: ${fetched.stderr.trim()}`);
259
+ }
260
+ const added = git(args.root, ["worktree", "add", "-b", branch, dir, base]);
261
+ if (!added.ok) throw new Error(`git worktree add failed: ${added.stderr.trim()}`);
262
+
263
+ return { isolated: true, worktreePath: path.resolve(args.root, dir), branch, base, skipReason: null, capabilities, commands };
264
+ }
265
+
266
+ function report(r: Result, dryRun: boolean): void {
267
+ const cap = r.capabilities;
268
+ console.log(`git repository: ${cap.gitRepo ? "yes" : "no"}`);
269
+ console.log(`gh: ${cap.gh ? (cap.ghAuthenticated ? "present, authenticated" : "present, NOT authenticated") : "absent"}`);
270
+ if (!cap.gh || !cap.ghAuthenticated) {
271
+ console.log(" -> Stage 4 takes the degraded path in references/implementation-delivery.md");
272
+ }
273
+ console.log("");
274
+ if (r.skipReason !== null) {
275
+ console.log("isolation SKIPPED, legal reason (record it verbatim in the completion report):");
276
+ console.log(` ${r.skipReason}`);
277
+ return;
278
+ }
279
+ for (const c of r.commands) console.log(` $ ${c}`);
280
+ console.log("");
281
+ if (dryRun) {
282
+ console.log("dry run — nothing was changed.");
283
+ return;
284
+ }
285
+ console.log("isolation evidence — record both in the session status now:");
286
+ console.log(` worktree path: ${r.worktreePath}`);
287
+ console.log(` branch: ${r.branch} (from ${r.base})`);
288
+ console.log("");
289
+ console.log("A fresh worktree has no node_modules, no dist and no .env. Provision it");
290
+ console.log("before the first gate; a gate that fails only for that is an environment");
291
+ console.log("failure, not a code failure.");
292
+ }
293
+
294
+ function main(argv: string[]): number {
295
+ const args = parseArgs(argv);
296
+ if (args === null) return 2;
297
+ let result: Result;
298
+ try {
299
+ result = ensure(args);
300
+ } catch (err) {
301
+ process.stderr.write(`ensure_worktree: ${(err as Error).message}\n`);
302
+ return 1;
303
+ }
304
+ if (args.json) console.log(JSON.stringify(result, null, 2));
305
+ else report(result, args.dryRun);
306
+ return 0;
307
+ }
308
+
309
+ if (import.meta.main) {
310
+ process.exit(main(process.argv.slice(2)));
311
+ }
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * resolve_scope.ts - deterministic audit scope packet.
4
+ *
5
+ * `references/audit-scope.md` requires every audit to produce a compact scope
6
+ * packet before analysis: scope type, target focus, resolution method, base and
7
+ * head, resolved files, exclusions, and a freshness timestamp. Assembling that
8
+ * by hand means running three or four git commands and transcribing their
9
+ * output, which is where a packet drifts from the tree it claims to describe.
10
+ * This emits the packet as JSON from the tree itself. Bun builtins only, zero
11
+ * dependencies, agent-agnostic.
12
+ *
13
+ * It resolves FILES, not judgment. It cannot decide whether a target focus is
14
+ * too vague to audit, and it does not try: `--target` is recorded verbatim, and
15
+ * the ask-when-vague rule stays with the agent and `references/audit-scope.md`.
16
+ *
17
+ * Exclusions are reported, never silently applied twice: the excluded paths are
18
+ * listed in the packet alongside the resolved files, so a reader can see what
19
+ * was dropped rather than inferring it from a count that does not add up.
20
+ *
21
+ * Usage:
22
+ * bun skills/massa-ai/scripts/resolve_scope.ts --scope modified
23
+ * bun skills/massa-ai/scripts/resolve_scope.ts --scope range --range abc123..def456
24
+ * bun skills/massa-ai/scripts/resolve_scope.ts --scope branch --base main
25
+ * bun skills/massa-ai/scripts/resolve_scope.ts --scope files --target 'src/**\/*.ts'
26
+ * bun skills/massa-ai/scripts/resolve_scope.ts --scope whole
27
+ *
28
+ * Exit codes: 0 resolved, 1 git failure or unresolvable scope, 2 usage error.
29
+ */
30
+
31
+ const SCOPES = ["modified", "range", "branch", "files", "whole"] as const;
32
+ type Scope = (typeof SCOPES)[number];
33
+
34
+ const USAGE =
35
+ "usage: resolve_scope.ts [-h] --scope {modified,range,branch,files,whole} [--range REV_RANGE] [--base REF] [--head REF] [--target TARGET] [--root ROOT]";
36
+ const HELP = `${USAGE}
37
+
38
+ Emit the shared audit scope packet from references/audit-scope.md as JSON.
39
+
40
+ options:
41
+ -h, --help show this help message and exit
42
+ --scope SCOPE one of: ${SCOPES.join(", ")}
43
+ --range REV_RANGE revision range for --scope range, e.g. abc123..def456
44
+ --base REF base ref for --scope branch (default: origin/HEAD, then main)
45
+ --head REF head ref for --scope branch (default: HEAD)
46
+ --target TARGET the target focus, recorded verbatim; a git pathspec for --scope files
47
+ (git's '*' crosses '/', so 'src/*.ts' also matches 'src/a/b.ts')
48
+ --root ROOT run git in this directory (default: cwd)`;
49
+
50
+ /**
51
+ * Paths excluded from every audit scope per references/audit-scope.md
52
+ * ("generated, dependency, build, log, cache, temporary, and secret paths").
53
+ * Matched as a path segment so `src/dist-helper.ts` is not caught by `dist`.
54
+ */
55
+ const EXCLUDED_SEGMENTS = [
56
+ "node_modules", "vendor", ".venv", "__pycache__", "dist", "build", ".next",
57
+ "out", "bin", "obj", "target", "Pods", ".gradle", ".expo", ".dart_tool",
58
+ "logs", ".cache", "tmp", ".git",
59
+ ];
60
+ const EXCLUDED_FILE_RE = /(?:\.min\.(?:js|css)|\.lock|\.log|\.pem|\.key|\.pyc|\.map)$/;
61
+
62
+ interface Args {
63
+ scope: Scope | null;
64
+ range: string | null;
65
+ base: string | null;
66
+ head: string | null;
67
+ target: string | null;
68
+ root: string;
69
+ }
70
+
71
+ function printUsageError(msg: string): void {
72
+ process.stderr.write(`${USAGE}\nresolve_scope.ts: error: ${msg}\n`);
73
+ }
74
+
75
+ function parseArgs(argv: string[]): Args | null {
76
+ const args: Args = { scope: null, range: null, base: null, head: null, target: null, root: "." };
77
+ const valued: Record<string, keyof Args> = {
78
+ "--scope": "scope", "--range": "range", "--base": "base",
79
+ "--head": "head", "--target": "target", "--root": "root",
80
+ };
81
+ for (let i = 0; i < argv.length; i++) {
82
+ const a = argv[i]!;
83
+ if (a === "-h" || a === "--help") {
84
+ console.log(HELP);
85
+ process.exit(0);
86
+ }
87
+ const eq = a.indexOf("=");
88
+ const flag = eq === -1 ? a : a.slice(0, eq);
89
+ const key = valued[flag];
90
+ if (key === undefined) {
91
+ printUsageError(`unrecognized argument: ${a}`);
92
+ return null;
93
+ }
94
+ let value: string;
95
+ if (eq !== -1) {
96
+ value = a.slice(eq + 1);
97
+ } else {
98
+ if (i + 1 >= argv.length) {
99
+ printUsageError(`argument ${flag}: expected one argument`);
100
+ return null;
101
+ }
102
+ value = argv[++i]!;
103
+ }
104
+ (args as Record<string, unknown>)[key] = value;
105
+ }
106
+ if (args.scope === null) {
107
+ printUsageError("argument --scope is required");
108
+ return null;
109
+ }
110
+ if (!(SCOPES as readonly string[]).includes(args.scope)) {
111
+ printUsageError(`argument --scope: invalid choice '${args.scope}' (choose from ${SCOPES.join(", ")})`);
112
+ return null;
113
+ }
114
+ if (args.scope === "range" && args.range === null) {
115
+ printUsageError("--scope range requires --range");
116
+ return null;
117
+ }
118
+ if (args.scope === "files" && args.target === null) {
119
+ printUsageError("--scope files requires --target");
120
+ return null;
121
+ }
122
+ return args;
123
+ }
124
+
125
+ /** Runs git and returns stdout, or null when git itself failed. */
126
+ function git(root: string, gitArgs: string[], quiet = false): string | null {
127
+ const proc = Bun.spawnSync(["git", ...gitArgs], { cwd: root, stdout: "pipe", stderr: "pipe" });
128
+ if (proc.exitCode !== 0) {
129
+ if (!quiet) process.stderr.write(new TextDecoder().decode(proc.stderr));
130
+ return null;
131
+ }
132
+ return new TextDecoder().decode(proc.stdout);
133
+ }
134
+
135
+ function lines(out: string | null): string[] {
136
+ return (out ?? "").split("\n").map((l) => l.trim()).filter(Boolean);
137
+ }
138
+
139
+ function isExcluded(p: string): boolean {
140
+ if (EXCLUDED_FILE_RE.test(p)) return true;
141
+ return p.split("/").some((seg) => EXCLUDED_SEGMENTS.includes(seg));
142
+ }
143
+
144
+ /**
145
+ * The base ref for a branch comparison, when the user named none.
146
+ *
147
+ * `origin/HEAD` is the repository's own answer and is preferred over guessing
148
+ * `main`; the fallbacks exist because a clone made with `--single-branch` has no
149
+ * `origin/HEAD` symref at all. Returning null rather than defaulting silently is
150
+ * deliberate: audit-scope.md says "Do not invent a base."
151
+ */
152
+ function defaultBase(root: string): string | null {
153
+ const symref = git(root, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"], true);
154
+ if (symref) return symref.trim().replace(/^refs\/remotes\//, "");
155
+ for (const candidate of ["origin/main", "origin/master", "main", "master"]) {
156
+ if (git(root, ["rev-parse", "--verify", "--quiet", candidate], true)) return candidate;
157
+ }
158
+ return null;
159
+ }
160
+
161
+ interface Packet {
162
+ scopeType: Scope;
163
+ targetFocus: string | null;
164
+ resolutionMethod: string;
165
+ base: string | null;
166
+ head: string | null;
167
+ files: string[];
168
+ excluded: string[];
169
+ freshness: string;
170
+ }
171
+
172
+ function resolve(args: Args): Packet | null {
173
+ const scope = args.scope!;
174
+ let method: string;
175
+ let base: string | null = null;
176
+ let head: string | null = null;
177
+ let raw: string[];
178
+
179
+ if (scope === "modified") {
180
+ method = "git status --porcelain --untracked-files=all (staged, unstaged, and untracked)";
181
+ head = "working-tree";
182
+ base = "HEAD";
183
+ // `--porcelain` covers untracked files, which `git diff --name-only` omits;
184
+ // audit-scope.md counts relevant untracked source as in scope.
185
+ // --untracked-files=all, not the default `normal`: git collapses an entirely
186
+ // untracked directory to a single `?? src/` entry, and a scope packet listing
187
+ // `src/` instead of the files under it under-reports the audit surface.
188
+ const out = git(args.root, ["status", "--porcelain", "--untracked-files=all"]);
189
+ if (out === null) return null;
190
+ // Not lines(): porcelain columns are `XY<space>path`, and X is a space for
191
+ // an unstaged-only change. Trimming the line first eats that column and
192
+ // then slice(3) eats the path's first character instead.
193
+ raw = out.split("\n").filter((l) => l.length > 3).map((l) => {
194
+ const path = l.slice(3);
195
+ // A rename is `R old -> new`; the audit target is the new path.
196
+ const arrow = path.indexOf(" -> ");
197
+ return arrow === -1 ? path : path.slice(arrow + 4);
198
+ });
199
+ } else if (scope === "range") {
200
+ method = `git diff --name-only ${args.range}`;
201
+ const [b, h] = args.range!.split("..");
202
+ base = b || null;
203
+ head = h || "HEAD";
204
+ const out = git(args.root, ["diff", "--name-only", args.range!]);
205
+ if (out === null) return null;
206
+ raw = lines(out);
207
+ } else if (scope === "branch") {
208
+ base = args.base ?? defaultBase(args.root);
209
+ if (base === null) {
210
+ process.stderr.write(
211
+ "resolve_scope: no base ref could be resolved and none was given. Pass --base; do not invent a base.\n",
212
+ );
213
+ return null;
214
+ }
215
+ head = args.head ?? "HEAD";
216
+ method = `git diff --name-only ${base}...${head} (merge-base three-dot)`;
217
+ const out = git(args.root, ["diff", "--name-only", `${base}...${head}`]);
218
+ if (out === null) return null;
219
+ raw = lines(out);
220
+ } else if (scope === "files") {
221
+ method = `git ls-files -- ${args.target}`;
222
+ const out = git(args.root, ["ls-files", "--", args.target!]);
223
+ if (out === null) return null;
224
+ raw = lines(out);
225
+ if (raw.length === 0) {
226
+ process.stderr.write(
227
+ `resolve_scope: pathspec '${args.target}' matched no tracked file. Ask for a concrete target.\n`,
228
+ );
229
+ return null;
230
+ }
231
+ } else {
232
+ method = "git ls-files (whole repository — explicitly requested scope only)";
233
+ const out = git(args.root, ["ls-files"]);
234
+ if (out === null) return null;
235
+ raw = lines(out);
236
+ }
237
+
238
+ const excluded = raw.filter(isExcluded).sort();
239
+ const files = raw.filter((p) => !isExcluded(p)).sort();
240
+ return {
241
+ scopeType: scope,
242
+ targetFocus: args.target,
243
+ resolutionMethod: method,
244
+ base,
245
+ head,
246
+ files,
247
+ excluded,
248
+ freshness: new Date().toISOString(),
249
+ };
250
+ }
251
+
252
+ function main(argv: string[]): number {
253
+ const args = parseArgs(argv);
254
+ if (args === null) return 2;
255
+ const packet = resolve(args);
256
+ if (packet === null) return 1;
257
+ console.log(JSON.stringify(packet, null, 2));
258
+ return 0;
259
+ }
260
+
261
+ if (import.meta.main) {
262
+ process.exit(main(process.argv.slice(2)));
263
+ }