create-pathfinder 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -79,11 +79,8 @@ An adapter carries the canonical skill's frontmatter and a pointer to it, and no
79
79
  - `reverse-engineer` — analyze an external reference and produce an evidence-based reconstruction blueprint
80
80
  - `prototype` — create and iterate the cheapest useful validation artifact
81
81
  - `to-specs` — generate context-sized feature specs
82
- - `load-feature` — prepare one feature for implementation
83
- - `start-feature` — implement scoped delivery chunks
82
+ - `feature` — run one action of the Feature delivery loop: `load`, `start`, `review`, `complete`
84
83
  - `debug-issue` — diagnose an observed failure to its root cause, apply the smallest justified fix, and verify it
85
- - `review-feature` — review against requirements, regressions, and standards
86
- - `complete-feature` — verify and close a feature cleanly
87
84
  - `learn-feature` — create an interactive lesson for a completed feature
88
85
  - `learn-codebase` — create a modular learning portal for the repository
89
86
  - `teach-feature` — teach the verified current feature from its spec, diff, tests, and implementation
@@ -38,10 +38,10 @@ Inspect current Git state before acting. If the workflow is unclear or
38
38
 
39
39
  Use the workflow skills instead of recreating their procedures in chat:
40
40
 
41
- 1. `load-feature` — load the active work and relevant context.
42
- 2. `start-feature` — implement the current delivery chunk.
43
- 3. `review-feature` — verify the work and report findings.
44
- 4. `complete-feature` — complete accepted work and durable records.
41
+ 1. `/feature load` — load the active work and relevant context.
42
+ 2. `/feature start` — implement the current delivery chunk.
43
+ 3. `/feature review` — verify the work and report findings.
44
+ 4. `/feature complete` — complete accepted work and durable records.
45
45
  5. `learn-feature` — optionally teach what was implemented.
46
46
 
47
47
  Roles are optional. The workflow must work without activating one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pathfinder",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Install the Pathfinder AI-assisted, human-in-the-loop workflow kit into a Git repository.",
5
5
  "keywords": [
6
6
  "pathfinder",
@@ -17,8 +17,7 @@ Do not load unrelated history, roadmap, Features, or repository areas by default
17
17
 
18
18
  ## Use
19
19
 
20
- - `load-feature` to load the work.
21
- - `start-feature` to implement it.
20
+ - `feature` — its load action to load the work, its start action to implement it.
22
21
  - Use the project's existing build, test, and verification commands as needed.
23
22
 
24
23
  ## Rules
package/roles/tester.md CHANGED
@@ -20,7 +20,7 @@ Do not rely on the developer's summary as proof.
20
20
 
21
21
  ## Use
22
22
 
23
- - `review-feature` to verify implemented work.
23
+ - `feature` — its review action, to verify implemented work.
24
24
  - Use the project's existing test commands and relevant testing tools.
25
25
 
26
26
  ## Rules
@@ -29,9 +29,9 @@ Use Debug Issue when there is a concrete unexpected behavior such as:
29
29
 
30
30
  Do not use it merely because implementation work is difficult.
31
31
 
32
- If the task is planned feature construction, use `start-feature`.
32
+ If the task is planned feature construction, use `/feature start`.
33
33
 
34
- If the goal is reviewing completed implementation for possible defects, use `review-feature`.
34
+ If the goal is reviewing completed implementation for possible defects, use `/feature review`.
35
35
 
36
36
  If the real question is broad understanding of the repository, use `learn-codebase`.
37
37
 
@@ -202,8 +202,8 @@ It does not:
202
202
 
203
203
  * implement unrelated feature scope
204
204
  * perform a general repository review
205
- * replace `review-feature`
206
- * replace `complete-feature`
205
+ * replace `/feature review`
206
+ * replace `/feature complete`
207
207
  * silently change architecture or dependencies
208
208
  * turn debugging into opportunistic refactoring
209
209
  * hide uncertainty behind a successful-looking workaround
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: feature
3
+ description: Run one action of the Feature delivery loop — load, start, review, or complete.
4
+ argument-hint: load|start|review|complete
5
+ ---
6
+
7
+ # Feature
8
+
9
+ The Feature delivery loop, as one skill. The human names the action:
10
+
11
+ `/feature load`
12
+ `/feature start`
13
+ `/feature review`
14
+ `/feature complete`
15
+
16
+ ## Process
17
+
18
+ 1. Take the action from the invocation.
19
+ If none was given, list the four actions below and stop.
20
+ If it is not one of the four, say so, list them, and stop.
21
+ 2. Read only `skills/feature/actions/<action>.md` and follow it exactly.
22
+
23
+ ## Actions
24
+
25
+ - `load` — prepare one Feature for execution. Reads its spec and the minimum
26
+ context around it, checks for blockers, records the approval in the spec, and
27
+ writes the session's workspace state.
28
+ - `start` — implement the active delivery chunk in small, stable increments,
29
+ restating the pre-implementation summary before editing anything.
30
+ - `review` — verify implemented work against its Feature and report findings.
31
+ It changes no implementation and accepts nothing.
32
+ - `complete` — complete work the human has accepted, through the project's
33
+ delivery workflow and its durable records.
34
+
35
+ ## Lifecycle
36
+
37
+ The Feature spec's `## Status` holds the durable lifecycle state named in
38
+ `context/ai-interaction.md`:
39
+
40
+ `Proposed` → `Ready` → `In Progress` → `Complete`
41
+
42
+ `Cancelled` and `Superseded` are terminal alternatives.
43
+
44
+ - `load` writes `Proposed` → `Ready`.
45
+ - `start` writes `Ready` → `In Progress`.
46
+ - `complete` writes `Complete`.
47
+ - `review` writes no status. Review is workflow activity, not lifecycle state.
48
+
49
+ `context/current-feature.md` is transient workspace state and records no status.
50
+
51
+ ## Rules
52
+
53
+ - Run the one action the human named. Do not continue into the next one.
54
+ - Read only that action's file. The other three are not context for this work.
55
+ - Human authority is unchanged: approval, acceptance, merge, and release are
56
+ the human's, whichever action is running.
@@ -0,0 +1,22 @@
1
+ # Feature: Complete
2
+
3
+ Invoking this action is the human's acceptance of the Feature. Do not ask for
4
+ acceptance again.
5
+
6
+ Acceptance of the Feature is not approval of the delivery steps below. Each one
7
+ that the project's documented workflow gates is still asked for.
8
+
9
+ 1. Run the project's required final checks.
10
+ 2. Follow the documented Git, merge, version, release, and deployment workflow.
11
+ Ask for approval where it requires it. If that workflow is undocumented or
12
+ `TBD`, stop and ask rather than choosing one.
13
+ 3. Mark the Feature `Complete` in its spec's `## Status`.
14
+ 4. Add a compact entry to `context/history.md`.
15
+ Create it from `templates/history.template.md` if it does not exist.
16
+ 5. Clear or update `context/current-feature.md`. Do nothing if it does not exist.
17
+ 6. If work tracking is configured, sync the completed state.
18
+ 7. Report the completed outcome and any remaining follow-up.
19
+
20
+ Do not re-review accepted work unless final verification exposes a new problem.
21
+
22
+ Do not claim completion if required checks or delivery steps failed.
@@ -0,0 +1,45 @@
1
+ # Feature: Load
2
+
3
+ Prepare one Feature for execution.
4
+
5
+ 1. Select the Feature the human named. Being invoked on it is the approval to
6
+ prepare it for execution, so its spec does not already have to say `Ready`.
7
+ Its spec is `NN-feature-name.md` in the spec source, and `NN` is its Feature
8
+ number.
9
+ 2. Read the Feature spec.
10
+ 3. Read only the files or context needed for the current work.
11
+ 4. Inspect the current Git state.
12
+ 5. Stop if a required human decision or explicit dependency blocks the work.
13
+ 6. Record the approval in the Feature spec's `## Status`, which holds the
14
+ durable lifecycle state:
15
+ - `Proposed` becomes `Ready`. That is the only value this action writes.
16
+ - `Ready` or `In Progress` is left exactly as it is. Reloading a Feature
17
+ mid-work is normal and must not rewrite its state.
18
+ - `Complete`, `Cancelled`, or `Superseded` blocks the load. Report it and
19
+ stop. Reopening terminal work is the human's decision.
20
+
21
+ Do this only once steps 1-5 found no blocker, and before the next step, so a
22
+ blocked load never leaves a promoted spec behind.
23
+ 7. Create or update `context/current-feature.md` — it does not ship, so the
24
+ first load writes it — with:
25
+ - Feature number, name, and spec path
26
+ - active delivery chunk
27
+ - Git state
28
+ - blocker, if any
29
+ - next action
30
+
31
+ Do not record the lifecycle status here. This file is transient workspace
32
+ state belonging to one session on one machine; the spec carries the durable
33
+ status.
34
+ 8. If `context/tracker.md` exists, name the tracked item for this Feature —
35
+ its key is that Feature number. Do nothing here if it does not.
36
+ 9. Present a short readiness summary.
37
+
38
+ Do not implement the Feature. That is `/feature start`.
39
+
40
+ Do not scan unrelated repository areas, load history or roadmap by default, or
41
+ silently resolve `TBD` decisions.
42
+
43
+ Do not rewrite the Feature's substance — its Goal, Context, Requirements, Out of
44
+ Scope, Delivery Chunks, or Acceptance Criteria. `## Status` is the one field this
45
+ action maintains.
@@ -1,9 +1,4 @@
1
- ---
2
- name: review-feature
3
- description: Verify implemented work against its Feature and report findings.
4
- ---
5
-
6
- # Review Feature
1
+ # Feature: Review
7
2
 
8
3
  Review the actual diff and behavior, not only the developer's summary.
9
4
 
@@ -31,3 +26,7 @@ Report:
31
26
  Do not modify the implementation unless the human explicitly asks.
32
27
 
33
28
  Do not invent findings or treat passing tests as automatic acceptance.
29
+
30
+ Do not write the Feature's `## Status`. Review is workflow activity, not
31
+ lifecycle state, and a reviewed Feature stays `In Progress` until it is
32
+ completed.
@@ -0,0 +1,33 @@
1
+ # Feature: Start
2
+
3
+ Implement the active delivery chunk.
4
+
5
+ 1. Read only the context needed for the current work.
6
+ 2. Restate the pre-implementation summary the project's agent guide requires,
7
+ and wait for it to be answered where it names a human decision. Do not edit
8
+ any file before this step is done.
9
+ 3. Record the Feature spec's `## Status` as `In Progress`, before the first file
10
+ edit of the first chunk:
11
+ - `Ready` becomes `In Progress`. That is the only value this action writes.
12
+ - `In Progress` is left exactly as it is. Continuing a Feature across
13
+ sessions and chunks is normal and must not rewrite its state.
14
+ - Any other status stops the session. Report it. A Feature that was never
15
+ loaded is not approved for execution, and terminal work is the human's to
16
+ reopen.
17
+ 4. Follow the Feature and existing project conventions.
18
+ 5. Make the smallest complete change.
19
+ 6. Verify the behavior you changed.
20
+ 7. Keep the repository stable.
21
+ 8. Create or update `context/current-feature.md` with the current state and next
22
+ action. Do not record the lifecycle status there; the spec carries it.
23
+
24
+ ## Rules
25
+
26
+ - Stay inside the approved scope.
27
+ - Stop if implementation requires a human decision or material scope change.
28
+ - Do not silently add dependencies or adopt prototype code.
29
+ - Follow the project's approval and Git rules.
30
+ - Do not decide that your own work is accepted.
31
+
32
+ When the chunk is done, report what changed, what was verified, and anything
33
+ unresolved.
@@ -173,7 +173,7 @@ Use one of these:
173
173
  * `debate-me` — when major product or technical decisions remain
174
174
  * `prototype` — when the experience or technical assumption needs validation
175
175
  * `to-specs` — when the direction is approved and ready to become feature files
176
- * `load-feature` — when the analysis applies to one already-planned feature
176
+ * `/feature load` — when the analysis applies to one already-planned feature
177
177
  * `learn-codebase` — when the user wants to understand their own repository instead
178
178
  * no handoff — when the user only requested analysis
179
179
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: role
3
3
  description: Activate one named Pathfinder role for the current session.
4
+ argument-hint: planner|developer|tester
4
5
  ---
5
6
 
6
7
  # Role
package/src/cli.mjs CHANGED
@@ -16,6 +16,7 @@ import { detectEditors, openInEditor } from "./editor.mjs";
16
16
  import { kickstartPrompt, kickstartPromptLines } from "./kickstart-prompt.mjs";
17
17
  import { createTheme } from "./theme.mjs";
18
18
  import { createProgress } from "./progress.mjs";
19
+ import { summarize } from "./outcome.mjs";
19
20
  import {
20
21
  HARNESSES,
21
22
  HARNESS_IDS,
@@ -48,6 +49,8 @@ Options:
48
49
  It does not authorize \`git init\` or configure any tool;
49
50
  pass --git-init and --agents for those.
50
51
  -h, --help Show this message.
52
+ -v, --version Print the version and exit, whatever else you passed.
53
+ Nothing else is printed.
51
54
 
52
55
  Adapters are generated files Pathfinder owns and regenerates without --force.
53
56
  A file it did not generate is never replaced, at any path, without --force.
@@ -82,6 +85,22 @@ export async function run(
82
85
  ) {
83
86
  const options = parseArguments(argv);
84
87
 
88
+ // Answered before the error branch, and before every other flag: `--version`
89
+ // is a question about this package, not about this run. It holds outside a Git
90
+ // repository, alongside a misspelled flag, and when stdout is redirected to a
91
+ // file or a pipe, because the one thing a caller asking for a version number
92
+ // can never use is a paragraph explaining why the version could not be
93
+ // printed. A reader that closes the pipe before this write lands still raises
94
+ // EPIPE, as it does for every other write here; that is not something this
95
+ // early return changes.
96
+ //
97
+ // One line, `VERSION` alone. No `v` prefix, no identity block, no findings:
98
+ // this output is read by scripts, and the plain form is the whole contract.
99
+ if (options.version) {
100
+ out(`${VERSION}\n`);
101
+ return 0;
102
+ }
103
+
85
104
  if (options.error) {
86
105
  err(`create-pathfinder: ${options.error}\n\n${USAGE}`);
87
106
  return 2;
@@ -270,7 +289,13 @@ export async function run(
270
289
  progress.finish();
271
290
  if (!options.dryRun && theme.tier !== "contract") out("\n");
272
291
 
273
- report({ result, plan, adapters, harnesses, customTools, cwd, gitRoot, options, out, err, theme });
292
+ // Derived here, once, and handed down. Below this line nothing re-reads a
293
+ // plan or a result: the two renderings disagree about everything except the
294
+ // facts, and this is what makes "except the facts" true rather than a hope
295
+ // about two functions being edited together.
296
+ const outcome = summarize({ plan, result, adapters, harnesses, options });
297
+
298
+ report({ outcome, harnesses, customTools, cwd, gitRoot, options, out, err, theme });
274
299
 
275
300
  // After the report, because the first offer is about the prompt the report
276
301
  // just printed — and because a question above the summary would make the user
@@ -286,7 +311,7 @@ export async function run(
286
311
  //
287
312
  // Not printed when anything failed. A sign-off over an error is a tool that
288
313
  // did not read its own output.
289
- const failed = result.errors.length > 0 || adapters.result.errors.length > 0;
314
+ const failed = outcome.failures.length > 0;
290
315
  if (theme.tier !== "contract" && !failed) {
291
316
  out(`\n ${theme.dim(SIGN_OFF)}\n`);
292
317
  }
@@ -497,9 +522,9 @@ function generateAdapters({ plan, harnesses, options, result, onProgress, onHarn
497
522
  * person reading has just watched a bar fill. A re-run that wrote nothing says
498
523
  * so plainly instead of inventing an achievement.
499
524
  */
500
- function endingHeadline({ theme, written, adapters, attention, options }) {
525
+ function endingHeadline({ theme, outcome, options }) {
501
526
  const mark = theme.glyph;
502
- const built = adapters.result.generated + adapters.result.replaced;
527
+ const { written, built, attention } = outcome;
503
528
 
504
529
  // Something wants a human. Still ready — it is — but this is not the moment
505
530
  // for confetti over somebody's conflicted file.
@@ -523,9 +548,9 @@ function endingHeadline({ theme, written, adapters, attention, options }) {
523
548
  * so plainly instead of inventing an achievement out of the harnesses it did
524
549
  * not have to configure.
525
550
  */
526
- function endingDetail({ written, adapters, harnesses, attention, options }) {
551
+ function endingDetail({ outcome, harnesses, options }) {
527
552
  const parts = [];
528
- const built = adapters.result.generated + adapters.result.replaced;
553
+ const { written, built, attention } = outcome;
529
554
 
530
555
  if (written > 0) parts.push(`${written} file${plural(written)}`);
531
556
  if (built > 0) parts.push(`${built} adapter${plural(built)}`);
@@ -563,21 +588,43 @@ function countWritten(result, plan, options) {
563
588
  return result.written + result.overwritten;
564
589
  }
565
590
 
591
+ /**
592
+ * Every option at its unasked-for default. `parseArguments` starts from a copy
593
+ * of this and `--version` returns one, so the two agree by construction.
594
+ */
595
+ const NO_OPTIONS = {
596
+ dryRun: false,
597
+ force: false,
598
+ help: false,
599
+ version: false,
600
+ gitInit: false,
601
+ noGitInit: false,
602
+ noClipboard: false,
603
+ noOpen: false,
604
+ yes: false,
605
+ // null means "not said", which is not the same as "none". Only the first
606
+ // suppresses the question.
607
+ agents: null,
608
+ error: null,
609
+ };
610
+
611
+ /**
612
+ * Which options this run asks for.
613
+ *
614
+ * `-v` and `--version` are recognized here, before any other token in `argv`
615
+ * is interpreted, because they ask what this package is rather than what this
616
+ * run should do. A misspelled flag or an invalid `--agents` value must not
617
+ * swallow the answer, and the answer must not depend on position: a wrapper
618
+ * that appends `--version` to arguments it was handed still gets a version
619
+ * number. That is why this is a scan and not another `case` below — the
620
+ * refusals in the loop return early, and one of them would otherwise win.
621
+ */
566
622
  function parseArguments(argv) {
567
- const options = {
568
- dryRun: false,
569
- force: false,
570
- help: false,
571
- gitInit: false,
572
- noGitInit: false,
573
- noClipboard: false,
574
- noOpen: false,
575
- yes: false,
576
- // null means "not said", which is not the same as "none". Only the first
577
- // suppresses the question.
578
- agents: null,
579
- error: null,
580
- };
623
+ if (argv.some((argument) => argument === "-v" || argument === "--version")) {
624
+ return { ...NO_OPTIONS, version: true };
625
+ }
626
+
627
+ const options = { ...NO_OPTIONS };
581
628
 
582
629
  for (let index = 0; index < argv.length; index += 1) {
583
630
  const argument = argv[index];
@@ -988,15 +1035,16 @@ export function formatFindings(findings, { theme = createTheme() } = {}) {
988
1035
  function report(args) {
989
1036
  // Two renderings, kept adjacent on purpose.
990
1037
  //
991
- // The duplication below is a known, accepted cost rather than an oversight.
992
- // `contractReport` owes byte-for-byte what 1.4.1 printed, to scripts that
993
- // parse it; `expressiveReport` owes a person a legible hierarchy. Merging
994
- // them would mean one function whose every line carries a conditional, and
995
- // the first wording improvement would silently break somebody's grep.
1038
+ // What is reported is decided once, in `summarize`: both functions below read
1039
+ // the same outcome and neither recounts anything. What remains separate is how
1040
+ // it looks. `contractReport` owes byte-for-byte what 1.4.1 printed, to scripts
1041
+ // that parse it; `expressiveReport` owes a person a legible hierarchy. Merging
1042
+ // them would mean one function whose every line carries a conditional, and the
1043
+ // first wording improvement would silently break somebody's grep.
996
1044
  //
997
- // They are written next to each other so that editing one is an obvious
998
- // prompt to consider the other. Anything that changes what is *reported*
999
- // as opposed to how it looks has to be made twice, and that is the point.
1045
+ // So a new reported fact belongs in the outcome, and reaches both renderings
1046
+ // from there; only its wording has to be chosen twice. They are written next
1047
+ // to each other so that editing one is an obvious prompt to consider the other.
1000
1048
  if (args.theme.tier === "contract") return contractReport(args);
1001
1049
  return expressiveReport(args);
1002
1050
  }
@@ -1008,7 +1056,7 @@ function report(args) {
1008
1056
  * `test/non-interactive.test.mjs` and by a capture-and-compare against the
1009
1057
  * published package, because a script somewhere is reading it.
1010
1058
  */
1011
- function contractReport({ result, plan, adapters, harnesses, customTools, cwd, gitRoot, options, out, err, theme }) {
1059
+ function contractReport({ outcome, harnesses, customTools, cwd, gitRoot, options, out, err, theme }) {
1012
1060
  const lines = [];
1013
1061
  const verb = options.dryRun ? "Would install" : "Installed";
1014
1062
 
@@ -1018,25 +1066,24 @@ function contractReport({ result, plan, adapters, harnesses, customTools, cwd, g
1018
1066
  }
1019
1067
  lines.push("");
1020
1068
 
1021
- const written = options.dryRun ? plan.filter((i) => i.status === "write").length : result.written;
1069
+ const { written, overwritten, skipped } = outcome;
1022
1070
  lines.push(` ${written} file${plural(written)} ${options.dryRun ? "to write" : "written"}`);
1023
1071
 
1024
- if (result.overwritten > 0) {
1025
- lines.push(` ${result.overwritten} file${plural(result.overwritten)} overwritten (--force)`);
1072
+ if (overwritten > 0) {
1073
+ lines.push(` ${overwritten} file${plural(overwritten)} overwritten (--force)`);
1026
1074
  }
1027
1075
 
1028
- lines.push(...contractAdapterLines({ adapters, harnesses, options, theme }));
1076
+ lines.push(...contractAdapterLines({ outcome, options, theme }));
1029
1077
  lines.push(...customToolLines(customTools));
1030
1078
 
1031
- const skipped = plan.filter((item) => item.status === "skip");
1032
1079
  if (skipped.length > 0) {
1033
1080
  lines.push(` ${skipped.length} file${plural(skipped.length)} left untouched because they already exist:`);
1034
- for (const item of skipped) lines.push(` ${item.relativePath}`);
1081
+ for (const path of skipped) lines.push(` ${path}`);
1035
1082
  lines.push("");
1036
1083
  lines.push(" Nothing above was modified. Re-run with --force to replace them.");
1037
1084
  }
1038
1085
 
1039
- if (written === 0 && skipped.length === plan.length) {
1086
+ if (outcome.alreadyInstalled) {
1040
1087
  lines.push("");
1041
1088
  lines.push("The kit is already installed here.");
1042
1089
  }
@@ -1054,7 +1101,7 @@ function contractReport({ result, plan, adapters, harnesses, customTools, cwd, g
1054
1101
 
1055
1102
  out(lines.join("\n") + "\n");
1056
1103
 
1057
- const failures = [...result.errors, ...adapters.result.errors];
1104
+ const { failures } = outcome;
1058
1105
  if (failures.length > 0) {
1059
1106
  const detail = failures.map((error) => ` ${error.relativePath}: ${error.message}`).join("\n");
1060
1107
  err(`\ncreate-pathfinder: ${failures.length} file${plural(failures.length)} could not be written:\n${detail}\n`);
@@ -1198,10 +1245,12 @@ async function offerEditor({ editors, cwd, prompter, out, env, platform, theme }
1198
1245
  * Empty when no harness was chosen, which is the default and must stay
1199
1246
  * invisible: a scripted 1.4.1-era run prints exactly what it always did.
1200
1247
  */
1201
- function contractAdapterLines({ adapters, harnesses, options, theme }) {
1202
- if (harnesses.length === 0) return [];
1203
-
1204
- if (adapters.blocked) {
1248
+ function contractAdapterLines({ outcome, options, theme }) {
1249
+ // No explicit "no harness chosen" guard: that case is no rows and
1250
+ // `blocked: false`, so it falls straight through to an empty list. The
1251
+ // blocked case is checked first because it is *also* no rows, and the two
1252
+ // must not print the same nothing.
1253
+ if (outcome.blocked) {
1205
1254
  return [
1206
1255
  "",
1207
1256
  " No adapters were generated, because the kit copy did not finish.",
@@ -1210,21 +1259,9 @@ function contractAdapterLines({ adapters, harnesses, options, theme }) {
1210
1259
  ];
1211
1260
  }
1212
1261
 
1213
- const failed = new Set(adapters.result.errors.map((error) => error.relativePath));
1214
1262
  const lines = [];
1215
1263
 
1216
- for (const harness of harnesses) {
1217
- const mine = adapters.plan.filter(
1218
- (item) => item.harness === harness && !failed.has(item.relativePath),
1219
- );
1220
- const count = (action) => mine.filter((item) => item.action === action).length;
1221
-
1222
- const generated = count("write");
1223
- const replaced = count("replace");
1224
- const unchanged = count("up-to-date");
1225
- const conflicts = mine.filter((item) => item.action === "conflict");
1226
- const orphans = mine.filter((item) => item.action === "orphan");
1227
-
1264
+ for (const { harness, generated, replaced, unchanged, conflicts, orphans } of outcome.harnessRows) {
1228
1265
  lines.push(
1229
1266
  ` ${generated} ${harness.label} skill adapter${plural(generated)} ` +
1230
1267
  (options.dryRun ? "to generate" : "generated"),
@@ -1242,7 +1279,7 @@ function contractAdapterLines({ adapters, harnesses, options, theme }) {
1242
1279
  lines.push(
1243
1280
  ` ${conflicts.length} file${plural(conflicts.length)} left untouched because Pathfinder did not write ${conflicts.length === 1 ? "it" : "them"}:`,
1244
1281
  );
1245
- for (const item of conflicts) lines.push(` ${item.relativePath}`);
1282
+ for (const path of conflicts) lines.push(` ${path}`);
1246
1283
  lines.push("");
1247
1284
  lines.push(
1248
1285
  conflicts.length === 1
@@ -1253,8 +1290,8 @@ function contractAdapterLines({ adapters, harnesses, options, theme }) {
1253
1290
  );
1254
1291
  }
1255
1292
 
1256
- for (const item of orphans) {
1257
- lines.push(` ${item.relativePath} delegates to a skill this version no longer`);
1293
+ for (const path of orphans) {
1294
+ lines.push(` ${path} delegates to a skill this version no longer`);
1258
1295
  lines.push(" ships. It was left in place; delete it yourself if you want it gone.");
1259
1296
  }
1260
1297
  }
@@ -1311,7 +1348,7 @@ function customToolLines(customTools = []) {
1311
1348
  * and get colour. The paths underneath get neither, for the reason below.
1312
1349
  * - **Diagnostics stay pasteable.** See `pathList`.
1313
1350
  */
1314
- function expressiveReport({ result, plan, adapters, harnesses, customTools, cwd, gitRoot, options, out, err, theme }) {
1351
+ function expressiveReport({ outcome, harnesses, customTools, cwd, gitRoot, options, out, err, theme }) {
1315
1352
  const mark = theme.glyph;
1316
1353
  const lines = [];
1317
1354
  const verb = options.dryRun ? "Would install" : "Installed";
@@ -1325,7 +1362,7 @@ function expressiveReport({ result, plan, adapters, harnesses, customTools, cwd,
1325
1362
  );
1326
1363
  }
1327
1364
 
1328
- const written = options.dryRun ? plan.filter((i) => i.status === "write").length : result.written;
1365
+ const { written, overwritten, skipped } = outcome;
1329
1366
  // A zero is reported, never celebrated. `✓ 0 files written` is a tick over
1330
1367
  // nothing happening, which is the kind of detail that makes a whole summary
1331
1368
  // feel automated rather than read.
@@ -1342,18 +1379,17 @@ function expressiveReport({ result, plan, adapters, harnesses, customTools, cwd,
1342
1379
  // was asked to do, and marking a requested action as a warning is how a tool
1343
1380
  // teaches people to ignore its warnings. The files it replaced are still
1344
1381
  // worth stating plainly, which is what `info` is for.
1345
- if (result.overwritten > 0) {
1382
+ if (overwritten > 0) {
1346
1383
  lines.push(
1347
1384
  railed(
1348
1385
  theme,
1349
- theme.info(`${mark.info} ${result.overwritten} file${plural(result.overwritten)} overwritten (--force)`),
1386
+ theme.info(`${mark.info} ${overwritten} file${plural(overwritten)} overwritten (--force)`),
1350
1387
  ),
1351
1388
  );
1352
1389
  }
1353
1390
 
1354
- lines.push(...expressiveAdapterLines({ adapters, harnesses, options, theme }));
1391
+ lines.push(...expressiveAdapterLines({ outcome, options, theme }));
1355
1392
 
1356
- const skipped = plan.filter((item) => item.status === "skip");
1357
1393
  if (skipped.length > 0) {
1358
1394
  lines.push(
1359
1395
  railed(
@@ -1379,33 +1415,26 @@ function expressiveReport({ result, plan, adapters, harnesses, customTools, cwd,
1379
1415
  theme,
1380
1416
  word: "Skipped",
1381
1417
  summary: `${skipped.length} file${plural(skipped.length)} already exist${skipped.length === 1 ? "s" : ""} and ${skipped.length === 1 ? "was" : "were"} left untouched`,
1382
- paths: skipped.map((item) => item.relativePath),
1418
+ paths: skipped,
1383
1419
  advice: ["Nothing above was modified. Re-run with --force to replace them."],
1384
1420
  }),
1385
1421
  );
1386
1422
  }
1387
1423
 
1388
- lines.push(...expressiveAdapterBlocks({ adapters, harnesses, theme }));
1424
+ lines.push(...expressiveAdapterBlocks({ outcome, theme }));
1389
1425
 
1390
1426
  if (customTools.length > 0) lines.push(...customToolLines(customTools));
1391
1427
 
1392
- const failureCount = result.errors.length + adapters.result.errors.length;
1393
- // What actually wants a human: a contested path, or an adapter pointing at a
1394
- // skill that is gone. Skipped files are deliberately *not* counted here.
1395
- // A re-run over an existing install skips every file by design, and calling
1396
- // thirty-six routine skips "things to look at" would turn the one number that
1397
- // should mean something into noise nobody reads twice.
1398
- const attention = adapters.blocked
1399
- ? 0
1400
- : adapters.plan.filter((item) => item.action === "conflict" || item.action === "orphan").length;
1401
-
1402
- if (failureCount === 0) {
1428
+ // `attention` is what actually wants a human: a contested path, or an adapter
1429
+ // pointing at a skill that is gone. Skipped files are deliberately not among
1430
+ // them see `summarize`, which is where that decision now lives.
1431
+ if (outcome.failures.length === 0) {
1403
1432
  lines.push("");
1404
1433
  lines.push(
1405
1434
  ...readyBlock({
1406
1435
  theme,
1407
- headline: endingHeadline({ theme, written, adapters, attention, options }),
1408
- detail: endingDetail({ written, adapters, harnesses, attention, options }),
1436
+ headline: endingHeadline({ theme, outcome, options }),
1437
+ detail: endingDetail({ outcome, harnesses, options }),
1409
1438
  }),
1410
1439
  );
1411
1440
  }
@@ -1431,7 +1460,7 @@ function expressiveReport({ result, plan, adapters, harnesses, customTools, cwd,
1431
1460
 
1432
1461
  out(lines.join("\n") + "\n");
1433
1462
 
1434
- const failures = [...result.errors, ...adapters.result.errors];
1463
+ const { failures } = outcome;
1435
1464
  if (failures.length > 0) {
1436
1465
  // `bad`, not `warn`, and the distinction is the whole point of having both:
1437
1466
  // everything above is an outcome somebody may want to know about, and this
@@ -1481,29 +1510,21 @@ function pathList(paths) {
1481
1510
  }
1482
1511
 
1483
1512
  /** The per-harness summary counts, on the gutter, each at its own severity. */
1484
- function expressiveAdapterLines({ adapters, harnesses, options, theme }) {
1513
+ function expressiveAdapterLines({ outcome, options, theme }) {
1485
1514
  const mark = theme.glyph;
1486
- if (harnesses.length === 0) return [];
1487
1515
 
1488
- if (adapters.blocked) {
1516
+ // No "no harness chosen" guard: that case is no rows and `blocked: false`,
1517
+ // so it falls through to an empty list. Blocked is checked first because it
1518
+ // is also no rows, and the two must not print the same nothing.
1519
+ if (outcome.blocked) {
1489
1520
  return [
1490
1521
  railed(theme, theme.bad(`${mark.bad} No adapters were generated`) + theme.dim(" (the kit copy did not finish)")),
1491
1522
  ];
1492
1523
  }
1493
1524
 
1494
- const failed = new Set(adapters.result.errors.map((error) => error.relativePath));
1495
1525
  const lines = [];
1496
1526
 
1497
- for (const harness of harnesses) {
1498
- const mine = adapters.plan.filter(
1499
- (item) => item.harness === harness && !failed.has(item.relativePath),
1500
- );
1501
- const count = (action) => mine.filter((item) => item.action === action).length;
1502
-
1503
- const generated = count("write");
1504
- const replaced = count("replace");
1505
- const unchanged = count("up-to-date");
1506
-
1527
+ for (const { harness, generated, replaced, unchanged, conflicts, orphans } of outcome.harnessRows) {
1507
1528
  lines.push(
1508
1529
  railed(
1509
1530
  theme,
@@ -1526,9 +1547,6 @@ function expressiveAdapterLines({ adapters, harnesses, options, theme }) {
1526
1547
  );
1527
1548
  }
1528
1549
 
1529
- const conflicts = mine.filter((item) => item.action === "conflict");
1530
- const orphans = mine.filter((item) => item.action === "orphan");
1531
-
1532
1550
  if (conflicts.length > 0) {
1533
1551
  lines.push(
1534
1552
  railed(
@@ -1554,20 +1572,12 @@ function expressiveAdapterLines({ adapters, harnesses, options, theme }) {
1554
1572
  }
1555
1573
 
1556
1574
  /** The conflict and orphan detail blocks, with their paths kept pasteable. */
1557
- function expressiveAdapterBlocks({ adapters, harnesses, theme }) {
1558
- if (harnesses.length === 0 || adapters.blocked) return [];
1575
+ function expressiveAdapterBlocks({ outcome, theme }) {
1576
+ if (outcome.blocked) return [];
1559
1577
 
1560
- const failed = new Set(adapters.result.errors.map((error) => error.relativePath));
1561
1578
  const blocks = [];
1562
1579
 
1563
- for (const harness of harnesses) {
1564
- const mine = adapters.plan.filter(
1565
- (item) => item.harness === harness && !failed.has(item.relativePath),
1566
- );
1567
-
1568
- const conflicts = mine.filter((item) => item.action === "conflict");
1569
- const orphans = mine.filter((item) => item.action === "orphan");
1570
-
1580
+ for (const { harness, conflicts, orphans } of outcome.harnessRows) {
1571
1581
  if (conflicts.length > 0) {
1572
1582
  const one = conflicts.length === 1;
1573
1583
  blocks.push(
@@ -1575,7 +1585,7 @@ function expressiveAdapterBlocks({ adapters, harnesses, theme }) {
1575
1585
  theme,
1576
1586
  word: "Conflict",
1577
1587
  summary: `${conflicts.length} ${harness.label} file${plural(conflicts.length)} at ${one ? "a path an adapter wants" : "paths adapters want"}, which Pathfinder did not write`,
1578
- paths: conflicts.map((item) => item.relativePath),
1588
+ paths: conflicts,
1579
1589
  advice: [
1580
1590
  `Re-run with --force to replace ${one ? "it" : "them"} ${theme.glyph.dash} note that --force also`,
1581
1591
  "overwrites Pathfinder kit files you have edited.",
@@ -1591,7 +1601,7 @@ function expressiveAdapterBlocks({ adapters, harnesses, theme }) {
1591
1601
  theme,
1592
1602
  word: "Orphan",
1593
1603
  summary: `${orphans.length} ${harness.label} adapter${plural(orphans.length)} delegat${one ? "es" : "e"} to a skill this version no longer ships`,
1594
- paths: orphans.map((item) => item.relativePath),
1604
+ paths: orphans,
1595
1605
  advice: [
1596
1606
  `Left in place. Delete ${one ? "it" : "them"} yourself if you want ${one ? "it" : "them"} gone.`,
1597
1607
  ],
package/src/kit.mjs CHANGED
@@ -68,7 +68,7 @@ export function isExcluded(basename) {
68
68
  *
69
69
  * `context/current-feature.md` and `context/handoff.md` are transient session
70
70
  * state, and they are here for the same reason one step further on: the kit
71
- * stopped shipping a blank `current-feature.md` stencil, because `load-feature`
71
+ * stopped shipping a blank `current-feature.md` stencil, because `/feature load`
72
72
  * writes the real one on first use and a placeholder is just a file a reader
73
73
  * has to recognise as empty. Having stopped shipping the blank one, the thing
74
74
  * to guard against is shipping a *filled-in* one — a destination project
@@ -0,0 +1,147 @@
1
+ /**
2
+ * What a run did, derived once.
3
+ *
4
+ * Two renderings print this install — `contractReport` owes byte-for-byte what
5
+ * 1.4.1 printed, `expressiveReport` owes a person a legible hierarchy — and
6
+ * they had each grown their own copy of the same four derivations: the
7
+ * mode-dependent written count, the skipped filter, the failure merge, and a
8
+ * per-harness adapter tally that appeared three times character-for-character.
9
+ * Four facts, ten spellings, and no mechanism keeping them in agreement. This
10
+ * module is the one spelling. The renderings stay two renderings; they just
11
+ * stop each deciding what the numbers are.
12
+ *
13
+ * Pure by construction: no filesystem, no `process`, no writing. Everything
14
+ * here is a function of the plans and results it is handed, which is what lets
15
+ * a summary be tested without building a temporary repository or a fake
16
+ * terminal.
17
+ *
18
+ * Two counters elsewhere are deliberately *not* folded in, and a later change
19
+ * that "finishes the job" will break them:
20
+ *
21
+ * - `countWritten` in `cli.mjs` runs mid-run, before adapters are applied, and
22
+ * reports a different number — `written + overwritten`, labelled "copied" —
23
+ * than the summary's `written`. It cannot read a result that does not exist
24
+ * yet.
25
+ * - The streaming per-harness counts in `generateAdapters` accumulate as units
26
+ * resolve, so milestones can print while the work is happening, and they
27
+ * count conflicts, orphans, and up-to-date adapters differently from the
28
+ * rows below. A summary computed at the end cannot drive a progress bar.
29
+ */
30
+
31
+ /**
32
+ * Every derived fact both renderings need, and nothing either of them can
33
+ * compute for itself.
34
+ *
35
+ * @param {object} args
36
+ * @param {{relativePath: string, status: "write"|"skip"|"overwrite"}[]} args.plan
37
+ * the kit copy plan, in `planInstall`'s sort
38
+ * @param {{written: number, skipped: number, overwritten: number,
39
+ * errors: {relativePath: string, message: string}[]}} args.result
40
+ * @param {{plan: object[], result: object, blocked: boolean}} args.adapters
41
+ * @param {{label: string}[]} args.harnesses the selected harnesses, registry order
42
+ * @param {{dryRun?: boolean}} args.options
43
+ * @returns {Readonly<object>} frozen; rows and lists frozen with it
44
+ */
45
+ export function summarize({ plan, result, adapters, harnesses, options }) {
46
+ // A dry run has no `result.written` to report, because nothing was written.
47
+ // The plan is counted instead, which is the same number the run would have
48
+ // produced had it been allowed to write.
49
+ const written = options.dryRun
50
+ ? plan.filter((item) => item.status === "write").length
51
+ : result.written;
52
+
53
+ const skipped = plan
54
+ .filter((item) => item.status === "skip")
55
+ .map((item) => item.relativePath);
56
+
57
+ // Copy errors before adapter errors, because that is the order they happened
58
+ // in and the order the failure list has always printed.
59
+ const failures = Object.freeze([...result.errors, ...adapters.result.errors]);
60
+
61
+ return Object.freeze({
62
+ written,
63
+ overwritten: result.overwritten,
64
+ skipped: Object.freeze(skipped),
65
+ // Nothing to write and every file already there. Not the same as `written
66
+ // === 0`, which a partly failed copy also satisfies.
67
+ alreadyInstalled: written === 0 && skipped.length === plan.length,
68
+ failures,
69
+ blocked: adapters.blocked,
70
+ // Summed from the result, never from `harnessRows`. The rows exclude paths
71
+ // that errored and this does not, so the two disagree exactly when a write
72
+ // fails — and this is the number the closing headline speaks for.
73
+ built: adapters.result.generated + adapters.result.replaced,
74
+ attention: attentionCount(adapters),
75
+ harnessRows: harnessRows({ adapters, harnesses }),
76
+ });
77
+ }
78
+
79
+ /**
80
+ * What actually wants a human: a contested path, or an adapter pointing at a
81
+ * skill that is gone.
82
+ *
83
+ * Skipped files are deliberately not counted. A re-run over an existing install
84
+ * skips every file by design, and calling thirty-six routine skips "things to
85
+ * look at" would turn the one number that should mean something into noise
86
+ * nobody reads twice.
87
+ *
88
+ * Counted across the whole adapter plan rather than across `harnessRows`,
89
+ * errored paths included, because a path that could not be written is still a
90
+ * path somebody has to go and look at.
91
+ */
92
+ function attentionCount(adapters) {
93
+ if (adapters.blocked) return 0;
94
+ return adapters.plan.filter(
95
+ (item) => item.action === "conflict" || item.action === "orphan",
96
+ ).length;
97
+ }
98
+
99
+ /**
100
+ * One row per selected harness, in the order the harnesses were given.
101
+ *
102
+ * The three-way distinction the report depends on is carried by the rows
103
+ * themselves, and all three collapse to a zero if it is lost:
104
+ *
105
+ * - no harness chosen — no rows, `blocked: false`
106
+ * - the kit copy failed — no rows, `blocked: true`
107
+ * - a harness that produced nothing — a row of zeroes
108
+ *
109
+ * The blocked case returns no rows explicitly rather than falling out of an
110
+ * empty plan, so that a harness which was chosen and never reached is never
111
+ * described as having generated zero adapters.
112
+ *
113
+ * Paths that failed to write are excluded from every count and list here: an
114
+ * adapter that could not be written was not generated, is not up to date, and
115
+ * is not a conflict the user can resolve by re-running with `--force`. They are
116
+ * reported once, as failures.
117
+ */
118
+ function harnessRows({ adapters, harnesses }) {
119
+ if (harnesses.length === 0 || adapters.blocked) return Object.freeze([]);
120
+
121
+ const failed = new Set(adapters.result.errors.map((error) => error.relativePath));
122
+
123
+ return Object.freeze(
124
+ harnesses.map((harness) => {
125
+ // Identity, not label: the harness object on a plan item is the registry
126
+ // entry itself, and two entries could plausibly share a label one day.
127
+ const mine = adapters.plan.filter(
128
+ (item) => item.harness === harness && !failed.has(item.relativePath),
129
+ );
130
+ const count = (action) => mine.filter((item) => item.action === action).length;
131
+ const paths = (action) =>
132
+ Object.freeze(
133
+ mine.filter((item) => item.action === action).map((item) => item.relativePath),
134
+ );
135
+
136
+ return Object.freeze({
137
+ harness,
138
+ generated: count("write"),
139
+ replaced: count("replace"),
140
+ unchanged: count("up-to-date"),
141
+ // `planAdapters` order, which is the order they will be printed in.
142
+ conflicts: paths("conflict"),
143
+ orphans: paths("orphan"),
144
+ });
145
+ }),
146
+ );
147
+ }
@@ -1,23 +0,0 @@
1
- ---
2
- name: complete-feature
3
- description: Complete accepted work through the project's delivery workflow and durable records.
4
- ---
5
-
6
- # Complete Feature
7
-
8
- Complete only work the human has accepted.
9
-
10
- 1. Confirm the Feature is accepted.
11
- 2. Run the project's required final checks.
12
- 3. Follow the documented Git, merge, version, release, and deployment workflow.
13
- Ask for approval where required.
14
- 4. Mark the Feature complete.
15
- 5. Add a compact entry to `context/history.md`.
16
- Create it from `templates/history.template.md` if it does not exist.
17
- 6. Clear or update `context/current-feature.md`. Do nothing if it does not exist.
18
- 7. If work tracking is configured, sync the completed state.
19
- 8. Report the completed outcome and any remaining follow-up.
20
-
21
- Do not re-review accepted work unless final verification exposes a new problem.
22
-
23
- Do not claim completion if required checks or delivery steps failed.
@@ -1,28 +0,0 @@
1
- ---
2
- name: load-feature
3
- description: Load one approved Feature and the minimum context needed to work on it.
4
- ---
5
-
6
- # Load Feature
7
-
8
- 1. Select the requested approved Feature. Its spec is
9
- `NN-feature-name.md` in the spec source, and `NN` is its Feature number.
10
- 2. Read the Feature spec.
11
- 3. Read only the files or context needed for the current work.
12
- 4. Inspect the current Git state.
13
- 5. Stop if a required human decision or explicit dependency blocks the work.
14
- 6. Create or update `context/current-feature.md` — it does not ship, so the
15
- first load writes it — with:
16
- - Feature number, name, and spec path
17
- - active delivery chunk
18
- - Git state
19
- - blocker, if any
20
- - next action
21
- 7. If `context/tracker.md` exists, name the tracked item for this Feature —
22
- its key is that Feature number. Do nothing here if it does not.
23
- 8. Present a short readiness summary.
24
-
25
- Do not implement the Feature.
26
-
27
- Do not scan unrelated repository areas, load history or roadmap by default,
28
- rewrite the Feature, or silently resolve `TBD` decisions.
@@ -1,29 +0,0 @@
1
- ---
2
- name: start-feature
3
- description: Implement the active Feature in small, stable increments.
4
- ---
5
-
6
- # Start Feature
7
-
8
- Implement the active delivery chunk.
9
-
10
- 1. Read only the context needed for the current work.
11
- 2. Restate the pre-implementation summary the project's agent guide requires,
12
- and wait for it to be answered where it names a human decision. Do not edit
13
- any file before this step is done.
14
- 3. Follow the Feature and existing project conventions.
15
- 4. Make the smallest complete change.
16
- 5. Verify the behavior you changed.
17
- 6. Keep the repository stable.
18
- 7. Create or update `context/current-feature.md` with the current state and next action.
19
-
20
- ## Rules
21
-
22
- - Stay inside the approved scope.
23
- - Stop if implementation requires a human decision or material scope change.
24
- - Do not silently add dependencies or adopt prototype code.
25
- - Follow the project's approval and Git rules.
26
- - Do not decide that your own work is accepted.
27
-
28
- When the chunk is done, report what changed, what was verified, and anything
29
- unresolved.