alignfirst 0.3.0 → 0.3.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # alignfirst
2
2
 
3
- The AlignFirst CLI provides collaborative software-development workflows, work files, and documentation discovery. Work files are organized by ticket and kept either git-ignored in the project or synchronized through a team plans repository.
3
+ The AlignFirst CLI provides collaborative software-development workflows, work files, and documentation discovery. Work files are organized by ticket and kept either git-ignored in the project or synchronized through a work-files repository.
4
4
 
5
5
  ## Agent skills
6
6
 
@@ -45,7 +45,7 @@ These examples use the `/` form. Replace it with `$` in Codex.
45
45
 
46
46
  To implement a plan, start a fresh agent context and ask it to execute the plan file.
47
47
 
48
- AlignFirst stores specifications, plans, and summaries in `.plans/<ticket-id>/`. It normally derives the ticket ID from the request or branch and asks when none is available. Files use a cycle letter and sequence number, such as `A1-spec.md` and `A2-plan.md`.
48
+ AlignFirst stores the work files of a ticket, such as specifications, plans and summaries, in `.plans/<ticket-id>/`. It normally derives the ticket ID from the request or branch and asks when none is available. Files use a cycle letter and sequence number, such as `A1-spec.md` and `A2-plan.md`.
49
49
 
50
50
  ## Global CLI
51
51
 
@@ -81,8 +81,8 @@ The guide installs the selected components and configures the repository. You ca
81
81
 
82
82
  - `guide` — Print an AlignFirst protocol.
83
83
  - `ticket` — Resolve a ticket directory, load its history, or get its next file.
84
- - `sync` — Synchronize shared plans.
85
- - `plans` — Set up, check and archive plans.
84
+ - `sync` — Synchronize the work files with the work-files repository.
85
+ - `plans` — Link `.plans` to the work-files repository, check the link, archive tickets.
86
86
  - `docmap` — Browse project documentation.
87
87
  - `conventions` — Print the effective project conventions.
88
88
  - `context` — Print the conventions, the documentation map when `docs/` exists, and the protocol aliases.
package/dist/cli.js CHANGED
@@ -39,7 +39,8 @@ export async function main(options) {
39
39
  ctx.projectConfig = resolveProjectConfig(ctx.cwd);
40
40
  checkCliRange(ctx.projectConfig?.config, ctx.version, [command, ...args]);
41
41
  }
42
- return dispatch(ctx, command, args);
42
+ const code = await dispatch(ctx, command, args);
43
+ return code;
43
44
  }
44
45
  catch (error) {
45
46
  if (!(error instanceof CliError))
@@ -55,7 +56,7 @@ function readPackageVersion() {
55
56
  return pkg.version;
56
57
  }
57
58
  function renderHelp(ctx) {
58
- return `alignfirst — protocols, plans and docs in one command.
59
+ return `alignfirst — protocols, work files and docs in one command.
59
60
 
60
61
  Usage:
61
62
  ${ctx.form} guide [<protocol>]
@@ -22,7 +22,7 @@ export function runDoctor(ctx, args) {
22
22
  return inspectConfig(ctx, resolved);
23
23
  });
24
24
  writeSection(ctx, "Git", () => inspectGit(ctx, resolved));
25
- writeSection(ctx, "Plans", () => inspectPlans(ctx));
25
+ writeSection(ctx, "Work files", () => inspectPlans(ctx));
26
26
  writeSection(ctx, "Docmap", () => inspectDocmap(ctx));
27
27
  writeSection(ctx, "Skills", () => inspectSkills(ctx));
28
28
  return 0;
@@ -51,10 +51,10 @@ function runSetup(ctx, args) {
51
51
  }
52
52
  function createPlansDirectory(cloneDir, folder) {
53
53
  if (folder.length === 0 || folder === "." || folder === ".." || /[\\/]/u.test(folder)) {
54
- throw new CliError(`Plans folder "${folder}" must be a single path segment.`);
54
+ throw new CliError(`Project folder "${folder}" must be a single path segment.`);
55
55
  }
56
56
  if (RESERVED_PLANS_FOLDERS.has(folder.toLowerCase())) {
57
- throw new CliError(`Plans folder "${folder}" is reserved.`);
57
+ throw new CliError(`Project folder "${folder}" is reserved.`);
58
58
  }
59
59
  const cloneRoot = realpathSync(cloneDir);
60
60
  const projectDir = join(cloneRoot, folder);
@@ -64,7 +64,7 @@ function createPlansDirectory(cloneDir, folder) {
64
64
  relativeTarget === ".." ||
65
65
  relativeTarget.startsWith(`..${sep}`) ||
66
66
  isAbsolute(relativeTarget)) {
67
- throw new CliError(`Plans folder "${folder}" must resolve inside ${cloneRoot}.`);
67
+ throw new CliError(`Project folder "${folder}" must resolve inside ${cloneRoot}.`);
68
68
  }
69
69
  return projectDir;
70
70
  }
@@ -94,11 +94,11 @@ function parseSetupArgs(ctx, args, usage) {
94
94
  }
95
95
  function checkClone(ctx, cloneDir) {
96
96
  if (!existsSync(cloneDir))
97
- throw new CliError(`${cloneDir} does not exist. Clone the team plans repository there first (see the instruction file).`);
97
+ throw new CliError(`${cloneDir} does not exist. Clone the work-files repository there first (see the instruction file).`);
98
98
  if (!existsSync(join(cloneDir, ".git")))
99
- throw new CliError(`${cloneDir} is not a git repository. Point ${ctx.form} plans setup at a clone of the team plans repository.`);
99
+ throw new CliError(`${cloneDir} is not a git repository. Point ${ctx.form} plans setup at a clone of the work-files repository.`);
100
100
  if (realpathSync(cloneDir) === realpathSync(ctx.cwd))
101
- throw new CliError(`${cloneDir} is the product repository itself. Point ${ctx.form} plans setup at a clone of the team plans repository.`);
101
+ throw new CliError(`${cloneDir} is the product repository itself. Point ${ctx.form} plans setup at a clone of the work-files repository.`);
102
102
  }
103
103
  function runCheck(ctx, args) {
104
104
  const usage = `Usage: ${ctx.form} plans check\n`;
@@ -111,9 +111,9 @@ function runCheck(ctx, args) {
111
111
  throw new CliError(renderStoppedRebase(stopped, ctx.form));
112
112
  }
113
113
  if (mode.kind === "shared")
114
- ctx.stdout.write(".plans is linked to the team plans repository.\n");
114
+ ctx.stdout.write(".plans is linked to the work-files repository.\n");
115
115
  else
116
- ctx.stdout.write(".plans is a local directory (local plans mode): synchronization is disabled.\n");
116
+ ctx.stdout.write(".plans is a local directory (local mode): synchronization is disabled.\n");
117
117
  return 0;
118
118
  }
119
119
  function runAutoArchive(ctx, args) {
@@ -1,2 +1,2 @@
1
1
  import type { CommandContext } from "../context.js";
2
- export declare function runSync(ctx: CommandContext, args: string[]): number;
2
+ export declare function runSync(ctx: CommandContext, args: string[]): Promise<number>;
@@ -6,7 +6,7 @@ import { parseCommandArgs } from "../parse-args.js";
6
6
  import { archiveThresholdDays, autoArchive } from "../plans/archive.js";
7
7
  import { resolvePlansMode } from "../plans/mode.js";
8
8
  import { findStoppedRebase, renderStoppedRebase, resolveStoppedRebase } from "../plans/rebase.js";
9
- export function runSync(ctx, args) {
9
+ export async function runSync(ctx, args) {
10
10
  const usage = `Usage: ${ctx.form} sync [--auto-archive | --no-auto-archive]\n`;
11
11
  const options = parseSyncArgs(ctx, args, usage);
12
12
  if (options === undefined)
@@ -22,40 +22,40 @@ export function runSync(ctx, args) {
22
22
  if (mode.kind === "local") {
23
23
  if (thresholdDays !== undefined)
24
24
  autoArchive(plansDir, thresholdDays, ctx.stdout);
25
- ctx.stdout.write("(local plans mode, nothing to sync)\n");
25
+ ctx.stdout.write("(local mode, nothing to sync)\n");
26
26
  return 0;
27
27
  }
28
28
  const repoDir = mode.repoToplevel;
29
29
  assertNoStoppedRebase(repoDir, ctx.form);
30
- git(repoDir, "add", "-A");
30
+ await git(ctx, repoDir, "add", "-A");
31
31
  if (hasStagedChanges(repoDir))
32
- git(repoDir, "commit", "--quiet", "-m", "sync");
32
+ await git(ctx, repoDir, "commit", "--quiet", "-m", "sync");
33
33
  if (hasUpstream(repoDir)) {
34
34
  try {
35
- git(repoDir, "pull", "--rebase");
35
+ await git(ctx, repoDir, "pull", "--rebase");
36
36
  }
37
37
  catch {
38
38
  if (findStoppedRebase(repoDir) === undefined)
39
39
  throw new CliError("git pull failed. See the git output above.");
40
- resolveStoppedRebase(repoDir, ctx.stdout);
40
+ await resolveStoppedRebase(ctx, repoDir);
41
41
  }
42
42
  }
43
43
  if (thresholdDays !== undefined && autoArchive(plansDir, thresholdDays, ctx.stdout)) {
44
- git(repoDir, "add", "-A");
44
+ await git(ctx, repoDir, "add", "-A");
45
45
  if (hasStagedChanges(repoDir))
46
- git(repoDir, "commit", "--quiet", "-m", "sync");
46
+ await git(ctx, repoDir, "commit", "--quiet", "-m", "sync");
47
47
  }
48
48
  if (hasCommitsToSend(repoDir)) {
49
49
  try {
50
- git(repoDir, "push", "--quiet", "-u", "origin", "HEAD");
50
+ await git(ctx, repoDir, "push", "--quiet", "-u", "origin", "HEAD");
51
51
  }
52
52
  catch {
53
53
  throw new CliError(`git push failed. See the git output above. Another synchronization may have landed first: run ${ctx.form} sync again.`);
54
54
  }
55
- ctx.stdout.write("Plans synchronized: local changes sent.\n");
55
+ ctx.stdout.write("Work files synchronized: local changes sent.\n");
56
56
  }
57
57
  else {
58
- ctx.stdout.write("Plans synchronized: nothing to send.\n");
58
+ ctx.stdout.write("Work files synchronized: nothing to send.\n");
59
59
  }
60
60
  return 0;
61
61
  }
package/dist/context.d.ts CHANGED
@@ -2,12 +2,14 @@ import type { ResolvedProjectConfig } from "./project-config.js";
2
2
  export interface Output {
3
3
  write(text: string): void;
4
4
  }
5
- export interface CommandContext {
5
+ export interface Streams {
6
+ stdout: Output;
7
+ stderr: Output;
8
+ }
9
+ export interface CommandContext extends Streams {
6
10
  cwd: string;
7
11
  env: NodeJS.ProcessEnv;
8
12
  home: string;
9
- stdout: Output;
10
- stderr: Output;
11
13
  form: string;
12
14
  version: string;
13
15
  projectConfig?: ResolvedProjectConfig;
@@ -49,17 +49,17 @@ function renderPlans(ctx) {
49
49
  try {
50
50
  const mode = resolvePlansMode(ctx.cwd, ctx.form);
51
51
  const folder = ctx.projectConfig?.config.plans?.folder;
52
- const sharedFolder = folder === undefined ? "" : ` (shared folder \`${folder}\`, a separate git repository)`;
52
+ const sharedFolder = folder === undefined ? "" : ` (folder \`${folder}\` in the work-files repository)`;
53
53
  const base = mode.kind === "shared"
54
- ? `Plans: use \`.plans\`${sharedFolder}; run \`${ctx.form} sync\` after changes.`
55
- : "Plans: use `.plans`.";
54
+ ? `Work files: use \`.plans\`${sharedFolder}; run \`${ctx.form} sync\` after changes.`
55
+ : "Work files: use `.plans`.";
56
56
  const archival = ctx.projectConfig?.config.plans?.autoArchive === true
57
57
  ? " Automatic archival is enabled."
58
58
  : "";
59
59
  return `${base}${archival}`;
60
60
  }
61
61
  catch (error) {
62
- return `Plans: ${errorMessage(error).split("\n", 1)[0]}`;
62
+ return `Work files: ${errorMessage(error).split("\n", 1)[0]}`;
63
63
  }
64
64
  }
65
65
  function plansEntryExists(cwd) {
package/dist/git.d.ts CHANGED
@@ -1,6 +1,8 @@
1
- export declare function git(dir: string, ...args: string[]): void;
1
+ import type { Streams } from "./context.js";
2
+ export declare function git(streams: Streams, dir: string, ...args: string[]): Promise<void>;
2
3
  export declare function assertMainWorktreeRoot(cwd: string): void;
3
4
  export declare function gitOutput(dir: string, ...args: string[]): string;
4
5
  export declare function gitOutputRaw(dir: string, ...args: string[]): string;
6
+ export declare function gitBuffer(dir: string, ...args: string[]): Buffer;
5
7
  export declare function gitOutputOrUndefined(dir: string, ...args: string[]): string | undefined;
6
8
  export declare function gitSucceeds(dir: string, ...args: string[]): boolean;
package/dist/git.js CHANGED
@@ -1,17 +1,33 @@
1
- import { execFileSync } from "node:child_process";
1
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
2
2
  import { realpathSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { CliError } from "./cli-error.js";
5
- export function git(dir, ...args) {
6
- try {
7
- execFileSync("git", ["-C", dir, ...args], { stdio: "inherit" });
8
- }
9
- catch {
10
- throw gitFailure(args);
11
- }
5
+ export function git(streams, dir, ...args) {
6
+ return new Promise((resolve, reject) => {
7
+ const child = spawn("git", ["-C", dir, ...args], {
8
+ stdio: ["inherit", "pipe", "pipe"],
9
+ });
10
+ child.stdout.setEncoding("utf8");
11
+ child.stderr.setEncoding("utf8");
12
+ child.stdout.on("data", (text) => streams.stdout.write(text));
13
+ child.stderr.on("data", (text) => streams.stderr.write(text));
14
+ child.once("error", (error) => reject(gitFailure(args, error.message)));
15
+ child.once("close", (code) => (code === 0 ? resolve() : reject(gitFailure(args))));
16
+ });
17
+ }
18
+ function gitFailure(args, detail) {
19
+ const output = detail?.trim();
20
+ const subcommand = gitSubcommand(args);
21
+ const label = subcommand === undefined ? "git command" : `git ${subcommand}`;
22
+ if (output === undefined || output === "")
23
+ return new CliError(`${label} failed. See the git output above.`);
24
+ return new CliError(`${label} failed:\n${output}`);
12
25
  }
13
- function gitFailure(args) {
14
- return new CliError(`git ${args[0]} failed. See the git output above.`);
26
+ function gitSubcommand(args) {
27
+ let index = 0;
28
+ while (args[index] === "-c")
29
+ index += 2;
30
+ return args[index];
15
31
  }
16
32
  export function assertMainWorktreeRoot(cwd) {
17
33
  const toplevel = gitOutput(cwd, "rev-parse", "--show-toplevel");
@@ -26,18 +42,20 @@ export function gitOutput(dir, ...args) {
26
42
  return gitOutputRaw(dir, ...args).trim();
27
43
  }
28
44
  export function gitOutputRaw(dir, ...args) {
29
- let output;
30
- try {
31
- output = execFileSync("git", ["-C", dir, ...args]);
32
- }
33
- catch {
34
- throw gitFailure(args);
35
- }
45
+ const output = gitBuffer(dir, ...args);
36
46
  const text = output.toString("utf8");
37
47
  if (!Buffer.from(text).equals(output))
38
48
  throw new CliError("Cannot read non-UTF-8 Git output safely. Resolve the rebase manually.");
39
49
  return text;
40
50
  }
51
+ export function gitBuffer(dir, ...args) {
52
+ const result = spawnSync("git", ["-C", dir, ...args]);
53
+ if (result.error !== undefined)
54
+ throw gitFailure(args, result.error.message);
55
+ if (result.status !== 0)
56
+ throw gitFailure(args, result.stderr.toString("utf8"));
57
+ return result.stdout;
58
+ }
41
59
  export function gitOutputOrUndefined(dir, ...args) {
42
60
  try {
43
61
  return execFileSync("git", ["-C", dir, ...args], {
@@ -1,2 +1,2 @@
1
- import type { Output } from "../context.js";
2
- export declare function resolveConflictedPaths(repoDir: string, stdout: Output): void;
1
+ import type { Streams } from "../context.js";
2
+ export declare function resolveConflictedPaths(streams: Streams, repoDir: string): void;
@@ -1,10 +1,9 @@
1
- import { execFileSync } from "node:child_process";
2
1
  import { chmodSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, } from "node:fs";
3
2
  import { basename, dirname, extname, join, relative, resolve } from "node:path";
4
3
  import { CliError } from "../cli-error.js";
5
- import { gitOutput, gitOutputRaw } from "../git.js";
4
+ import { gitBuffer, gitOutput, gitOutputRaw } from "../git.js";
6
5
  import { nextFilePosition } from "./ticket.js";
7
- export function resolveConflictedPaths(repoDir, stdout) {
6
+ export function resolveConflictedPaths(streams, repoDir) {
8
7
  const conflicts = readConflicts(repoDir);
9
8
  const remote = readTree(repoDir, "HEAD");
10
9
  const local = readTree(repoDir, "REBASE_HEAD");
@@ -35,7 +34,7 @@ export function resolveConflictedPaths(repoDir, stdout) {
35
34
  applyResolution(repoDir, conflicts, resolution);
36
35
  for (const path of conflicts.keys()) {
37
36
  const renamed = resolution.renames.get(path);
38
- stdout.write(renamed === undefined
37
+ streams.stdout.write(renamed === undefined
39
38
  ? `Resolved ${path}: preserved committed contents at the surviving paths.\n`
40
39
  : `Resolved ${path}: kept the published version; saved the local version as ${renamed}.\n`);
41
40
  }
@@ -49,7 +48,7 @@ function readConflicts(repoDir) {
49
48
  const header = entry.slice(0, tab).split(" ");
50
49
  const stage = Number(header[2]);
51
50
  if (tab === -1 || ![1, 2, 3].includes(stage))
52
- throw new CliError("Could not read the conflicted plans index.");
51
+ throw new CliError("Could not read the git index of the work-files repository.");
53
52
  const path = entry.slice(tab + 1);
54
53
  const stages = result.get(path) ?? new Set();
55
54
  stages.add(stage);
@@ -177,7 +176,7 @@ function assertNoLaterEdits(repoDir, renames) {
177
176
  }
178
177
  }
179
178
  function readBlob(repoDir, blob) {
180
- return execFileSync("git", ["-C", repoDir, "cat-file", "blob", blob.id]);
179
+ return gitBuffer(repoDir, "cat-file", "blob", blob.id);
181
180
  }
182
181
  function updateReferences(original, source, renames) {
183
182
  const replacements = new Map();
@@ -187,7 +186,7 @@ function updateReferences(original, source, renames) {
187
186
  const to = relative(dirname(source), after);
188
187
  replacements.set(from, to);
189
188
  replacements.set(`./${from}`, `./${to}`);
190
- // A shared plans clone stores the project's .plans directory under its configured folder.
189
+ // A work-files repository clone stores the project's .plans directory under its configured folder.
191
190
  replacements.set(before.replace(/^[^/]+\//, ".plans/"), after.replace(/^[^/]+\//, ".plans/"));
192
191
  }
193
192
  if (replacements.size === 0)
@@ -25,7 +25,7 @@ export function assertPlansGate(cwd, form) {
25
25
  return stats;
26
26
  }
27
27
  export function missingPlansMessage(form) {
28
- return `No .plans/ directory in the current directory.\nLocal plans: mkdir .plans && echo .plans >> .gitignore\nTeam plans: ${form} plans setup <clone-dir>`;
28
+ return `No .plans/ directory in the current directory.\nLocal work files: mkdir .plans && echo .plans >> .gitignore\nTeam work files: ${form} plans setup <clone-dir>`;
29
29
  }
30
30
  export function missingPlansError(form) {
31
31
  return new CliError(missingPlansMessage(form));
@@ -6,7 +6,7 @@ export function linkPlans(ctx, targetDir) {
6
6
  const stats = lstatSync(plansPath, { throwIfNoEntry: false });
7
7
  if (stats?.isSymbolicLink()) {
8
8
  if (existsSync(plansPath) && realpathSync(plansPath) === realpathSync(targetDir)) {
9
- ctx.stdout.write(".plans already links to the plans repository.\n");
9
+ ctx.stdout.write(".plans already links to the work-files repository.\n");
10
10
  return;
11
11
  }
12
12
  rmSync(plansPath);
@@ -1,8 +1,8 @@
1
- import type { Output } from "../context.js";
1
+ import type { Streams } from "../context.js";
2
2
  export interface StoppedRebase {
3
3
  repoDir: string;
4
4
  conflictedFiles: string[];
5
5
  }
6
6
  export declare function findStoppedRebase(repoDir: string): StoppedRebase | undefined;
7
- export declare function resolveStoppedRebase(repoDir: string, stdout: Output): void;
7
+ export declare function resolveStoppedRebase(streams: Streams, repoDir: string): Promise<void>;
8
8
  export declare function renderStoppedRebase(stopped: StoppedRebase, form: string): string;
@@ -17,26 +17,26 @@ export function findStoppedRebase(repoDir) {
17
17
  conflictedFiles: conflicts.split("\0").filter((path) => path !== ""),
18
18
  };
19
19
  }
20
- export function resolveStoppedRebase(repoDir, stdout) {
20
+ export async function resolveStoppedRebase(streams, repoDir) {
21
21
  for (let step = 0; findStoppedRebase(repoDir) !== undefined; ++step) {
22
22
  if (step >= MAX_REBASE_STEPS)
23
23
  throw new CliError(`Could not finish the stopped rebase in ${repoDir}.`);
24
- resolveConflictedPaths(repoDir, stdout);
25
- git(repoDir, "add", "-A");
26
- continueRebase(repoDir);
24
+ resolveConflictedPaths(streams, repoDir);
25
+ await git(streams, repoDir, "add", "-A");
26
+ await continueRebase(streams, repoDir);
27
27
  }
28
28
  }
29
- function continueRebase(repoDir) {
29
+ async function continueRebase(streams, repoDir) {
30
30
  const args = ["-c", "core.editor=true", "rebase", "--continue"];
31
31
  try {
32
- git(repoDir, ...args);
32
+ await git(streams, repoDir, ...args);
33
33
  }
34
34
  catch (error) {
35
35
  const stopped = findStoppedRebase(repoDir);
36
36
  if (stopped?.conflictedFiles.length)
37
37
  return;
38
38
  if (stopped && gitSucceeds(repoDir, "diff", "--cached", "--quiet")) {
39
- git(repoDir, "rebase", "--skip");
39
+ await git(streams, repoDir, "rebase", "--skip");
40
40
  return;
41
41
  }
42
42
  throw error;
@@ -45,7 +45,7 @@ function continueRebase(repoDir) {
45
45
  export function renderStoppedRebase(stopped, form) {
46
46
  const files = stopped.conflictedFiles.map((path) => ` ${path}`);
47
47
  return [
48
- `Plans synchronization stopped on a conflict in ${stopped.repoDir}:`,
48
+ `Work-files synchronization stopped on a conflict in ${stopped.repoDir}:`,
49
49
  ...files,
50
50
  "Resolve the markers in these files, then run:",
51
51
  ` git -C ${stopped.repoDir} add -A && git -C ${stopped.repoDir} rebase --continue`,
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "alignfirst",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "license": "CC0-1.0",
5
5
  "author": "Thomas MUR",
6
- "description": "The AlignFirst CLI: protocols, plans and docs in one command.",
6
+ "description": "The AlignFirst CLI: protocols, work files and docs in one command.",
7
7
  "keywords": [
8
8
  "alignfirst",
9
9
  "cli",
@@ -5,7 +5,7 @@ You are one of several reviewers examining the same branch, each from a differen
5
5
  ## Scope
6
6
 
7
7
  - Review the changes between the merge-base and HEAD (the orchestrator gives you both): `git diff <merge_base> HEAD`. The review target is the branch as committed.
8
- - Fresh eyes: derive everything from the code and the diff. Do not read specs, plans, summaries, or any file in the task directory (e.g., under `.plans/`).
8
+ - Fresh eyes: derive everything from the code and the diff. Do not read specs, plans, summaries, or any file in the ticket directory (under `.plans/`).
9
9
  - Read-only: never modify the working tree, the index, or HEAD.
10
10
 
11
11
  ## Method