create-pathfinder 1.8.0 → 2.1.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/AGENTS.md +3 -1
- package/CLAUDE.md +20 -0
- package/README.md +383 -65
- package/context/ai-interaction.md +78 -39
- package/context/coding-standards.md +119 -60
- package/copy-list.json +1 -0
- package/package.json +2 -1
- package/roles/developer.md +37 -0
- package/roles/planner.md +38 -0
- package/roles/tester.md +40 -0
- package/skills/challenge-me/SKILL.md +11 -7
- package/skills/complete-feature/SKILL.md +16 -11
- package/skills/handoff/SKILL.md +24 -4
- package/skills/kickstart-pathfinder/SKILL.md +6 -1
- package/skills/learning-review/SKILL.md +9 -6
- package/skills/load-feature/SKILL.md +40 -10
- package/skills/prototype/SKILL.md +3 -1
- package/skills/quiz-me/SKILL.md +33 -7
- package/skills/review-feature/SKILL.md +19 -11
- package/skills/role/SKILL.md +33 -0
- package/skills/setup-tracker/SKILL.md +29 -61
- package/skills/start-feature/SKILL.md +19 -16
- package/skills/sync-tracker/SKILL.md +67 -106
- package/skills/teach-architecture/SKILL.md +6 -5
- package/skills/teach-feature/SKILL.md +11 -8
- package/skills/to-specs/SKILL.md +40 -28
- package/skills/whereami/SKILL.md +87 -0
- package/src/cli.mjs +67 -96
- package/src/install.mjs +5 -1
- package/src/kit.mjs +70 -1
- package/src/outcome.mjs +147 -0
- package/templates/CHANGELOG.template.md +2 -1
- package/templates/feature-spec.template.md +18 -80
- package/templates/history.template.md +12 -0
- package/templates/lesson.template.md +22 -51
- package/templates/project-overview.template.md +80 -145
- package/context/current-feature.md +0 -46
- package/context/features/example-feature-spec.md +0 -103
- package/context/history.md +0 -14
- package/context/learning/learner-profile.md +0 -35
- package/context/learning/lessons/.gitkeep +0 -0
- package/context/learning/progress.md +0 -28
- package/context/project-overview.md +0 -211
- package/templates/progress-entry.template.md +0 -23
- package/templates/tracker.template.md +0 -359
|
@@ -13,13 +13,14 @@ The goal is not to produce generic documentation. The goal is to help the learne
|
|
|
13
13
|
|
|
14
14
|
Read only the smallest useful context:
|
|
15
15
|
|
|
16
|
-
1. `context/
|
|
17
|
-
2. `context/
|
|
18
|
-
3.
|
|
19
|
-
4.
|
|
20
|
-
5.
|
|
21
|
-
6.
|
|
22
|
-
|
|
16
|
+
1. `context/current-feature.md`
|
|
17
|
+
2. The source feature spec under `context/features/`
|
|
18
|
+
3. `context/project-overview.md`
|
|
19
|
+
4. The current branch, working tree status, and focused diff
|
|
20
|
+
5. Tests and implementation files touched by the feature
|
|
21
|
+
6. Relevant ADR, coding standard, or design-system guidance only when it materially affects the feature
|
|
22
|
+
|
|
23
|
+
Skip any of these that does not exist. Pathfinder creates these files only when a workflow first needs them, so their absence is normal and is not an error. Do not create them just to satisfy this list.
|
|
23
24
|
|
|
24
25
|
Do not read the entire repository by default.
|
|
25
26
|
|
|
@@ -57,7 +58,9 @@ Explain:
|
|
|
57
58
|
|
|
58
59
|
## Adaptation
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
Pitch the lesson at what the learner has already demonstrated, using
|
|
62
|
+
`context/learning/progress.md` when it exists and what the human tells you when
|
|
63
|
+
it does not. Ask rather than assume.
|
|
61
64
|
|
|
62
65
|
For an experienced engineer:
|
|
63
66
|
|
package/skills/to-specs/SKILL.md
CHANGED
|
@@ -1,49 +1,61 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: to-specs
|
|
3
|
-
description:
|
|
3
|
+
description: Turn approved direction into small, clear Feature specs.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# To Specs
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Turn approved direction into the minimum Feature specs needed to implement it.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## Process
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
1. Read the approved project context and relevant existing Features.
|
|
13
|
+
2. Stop if a required human decision is still `TBD`.
|
|
14
|
+
3. Decide whether the work is one Feature or needs a small number of Features.
|
|
15
|
+
4. Create `context/features/` if it does not exist.
|
|
16
|
+
5. Create each Feature from `templates/feature-spec.template.md`, named
|
|
17
|
+
`NN-feature-name.md` — see Naming below.
|
|
18
|
+
6. Fill only information that materially helps implementation and review.
|
|
19
|
+
7. Present the created Features and recommend which one to start first.
|
|
20
|
+
8. If `context/tracker.md` exists, offer to publish them with `sync-tracker`.
|
|
21
|
+
Do nothing here if it does not.
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
## Naming
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
A Feature spec is named `NN-feature-name.md` in the spec source, for example
|
|
26
|
+
`context/features/27-export-saved-searches.md`.
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
`NN` is the Feature number: the next unused number in the spec source, counting
|
|
29
|
+
every spec already there whatever its status. Numbers are never reused and never
|
|
30
|
+
renumbered, because a published tracker item is matched on that number and
|
|
31
|
+
renumbering would orphan it.
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- be independently reviewable and verifiable
|
|
27
|
-
- require a coherent, bounded set of context
|
|
28
|
-
- state dependencies, assumptions, and exclusions
|
|
29
|
-
- contain stable delivery chunks
|
|
33
|
+
The number lives in the filename and nowhere else. The Feature template carries
|
|
34
|
+
no number field, and nothing else in the spec records one.
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
## Sizing
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
Prefer one Feature when the work is coherent.
|
|
34
39
|
|
|
35
|
-
|
|
40
|
+
Split only when separate outcomes:
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
- can be implemented or reviewed independently
|
|
43
|
+
- depend on meaningfully different context
|
|
44
|
+
- have a real dependency between them
|
|
45
|
+
- would make one Feature unnecessarily difficult to understand
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
Prefer fewer Features and fewer artifacts.
|
|
40
48
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
If `context/tracker.md` exists, offer to publish the new specs with `sync-tracker`. If it does not, say nothing about tracking.
|
|
49
|
+
Use delivery chunks inside a Feature before creating additional Features.
|
|
44
50
|
|
|
45
51
|
## Rules
|
|
46
52
|
|
|
47
|
-
- Do not implement
|
|
48
|
-
- Do not
|
|
49
|
-
- Do not plan the
|
|
53
|
+
- Do not implement.
|
|
54
|
+
- Do not invent unresolved decisions.
|
|
55
|
+
- Do not plan beyond the approved direction.
|
|
56
|
+
- Do not add workflow metadata that the Feature template does not require.
|
|
57
|
+
- Do not create tickets, tracker items, or extra planning files by default.
|
|
58
|
+
- Do not scan unrelated repository areas.
|
|
59
|
+
|
|
60
|
+
When implementation details are unclear, inspect only enough source code to
|
|
61
|
+
write an accurate Feature.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: whereami
|
|
3
|
+
description: Report a compact read-only snapshot of the current Pathfinder work session.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Where Am I
|
|
7
|
+
|
|
8
|
+
Answer one question: what is this session working on right now?
|
|
9
|
+
|
|
10
|
+
Use it when a session resumes, after a long gap, or before deciding the next
|
|
11
|
+
action. It reports state. It never changes it.
|
|
12
|
+
|
|
13
|
+
## Process
|
|
14
|
+
|
|
15
|
+
1. Role: report the role activated in this session via `/role`.
|
|
16
|
+
Roles are session state and are never written to disk, so if no role was
|
|
17
|
+
activated in this conversation, report `none`. Do not search `roles/`.
|
|
18
|
+
2. Read `context/current-feature.md` if it exists.
|
|
19
|
+
Take Feature, Active chunk, and Next from it verbatim. The Feature number is
|
|
20
|
+
the `NN` recorded there, from its spec filename.
|
|
21
|
+
If the file is missing or still holds template placeholders, report `none`.
|
|
22
|
+
3. Run `git status --short --branch` once.
|
|
23
|
+
Report the branch/ref, and `clean` or the count of changed paths.
|
|
24
|
+
4. Compare the Git section of `context/current-feature.md` with step 3.
|
|
25
|
+
Report a drift line only if the recorded branch differs from the real one.
|
|
26
|
+
5. Context telemetry: report it only if this harness exposes it.
|
|
27
|
+
Otherwise `unavailable`. Do not estimate.
|
|
28
|
+
|
|
29
|
+
## Output
|
|
30
|
+
|
|
31
|
+
Exactly this shape, one line each:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Role: <role | none>
|
|
35
|
+
Feature: <## — name | none>
|
|
36
|
+
Chunk: <number and name | none>
|
|
37
|
+
Git: <branch/ref> — <clean | N changed>
|
|
38
|
+
Context: <telemetry | unavailable>
|
|
39
|
+
Next: <single next action | none>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Add at most one line after it, and only when step 4 found drift:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
Drift: current-feature.md records <branch>, working tree is on <branch>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then stop.
|
|
49
|
+
|
|
50
|
+
## Rules
|
|
51
|
+
|
|
52
|
+
- Read only. No writes, no commits, no `git` command that mutates anything.
|
|
53
|
+
- Read at most one file: `context/current-feature.md`.
|
|
54
|
+
- Do not open the feature spec, history, roadmap, `.features/`, or source.
|
|
55
|
+
- Report `none` or `unavailable` instead of inferring a missing value.
|
|
56
|
+
- Do not offer to fix drift, update state, or start the next action.
|
|
57
|
+
The human decides what happens after the snapshot.
|
|
58
|
+
|
|
59
|
+
## Example
|
|
60
|
+
|
|
61
|
+
`/whereami` after resuming mid-feature:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
Role: developer
|
|
65
|
+
Feature: 12 — export saved searches
|
|
66
|
+
Chunk: 2 — CSV writer
|
|
67
|
+
Git: feature/12-export-saved-searches — 3 changed
|
|
68
|
+
Context: unavailable
|
|
69
|
+
Next: Verify the CSV writer against the acceptance criteria
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Anti-example
|
|
73
|
+
|
|
74
|
+
Do not do this:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
Role: developer (inferred from recent commits)
|
|
78
|
+
Feature: 12 — export saved searches
|
|
79
|
+
Chunk: 3 — probably the download endpoint
|
|
80
|
+
Git: feature/12-export-saved-searches — 3 changed
|
|
81
|
+
Context: ~60% used
|
|
82
|
+
Next: I can update current-feature.md and start chunk 3 — want me to?
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
It guesses the role from history, invents a chunk the spec never named,
|
|
86
|
+
estimates telemetry it cannot see, and turns a status report into a proposal
|
|
87
|
+
to write state.
|
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,
|
|
@@ -205,7 +206,8 @@ export async function run(
|
|
|
205
206
|
// as it goes.
|
|
206
207
|
//
|
|
207
208
|
// Planning adapters this early is safe, and specifically because of what the
|
|
208
|
-
// copy list contains: AGENTS.md, CLAUDE.md, context, skills, and
|
|
209
|
+
// copy list contains: AGENTS.md, CLAUDE.md, context, roles, skills, and
|
|
210
|
+
// templates.
|
|
209
211
|
// No entry writes into `.claude/` or `.agents/`, so the copy cannot change
|
|
210
212
|
// the answer `planAdapters` gives about an adapter path, and the canonical
|
|
211
213
|
// skills it reads come from the kit rather than from the destination. If a
|
|
@@ -269,7 +271,13 @@ export async function run(
|
|
|
269
271
|
progress.finish();
|
|
270
272
|
if (!options.dryRun && theme.tier !== "contract") out("\n");
|
|
271
273
|
|
|
272
|
-
|
|
274
|
+
// Derived here, once, and handed down. Below this line nothing re-reads a
|
|
275
|
+
// plan or a result: the two renderings disagree about everything except the
|
|
276
|
+
// facts, and this is what makes "except the facts" true rather than a hope
|
|
277
|
+
// about two functions being edited together.
|
|
278
|
+
const outcome = summarize({ plan, result, adapters, harnesses, options });
|
|
279
|
+
|
|
280
|
+
report({ outcome, harnesses, customTools, cwd, gitRoot, options, out, err, theme });
|
|
273
281
|
|
|
274
282
|
// After the report, because the first offer is about the prompt the report
|
|
275
283
|
// just printed — and because a question above the summary would make the user
|
|
@@ -285,7 +293,7 @@ export async function run(
|
|
|
285
293
|
//
|
|
286
294
|
// Not printed when anything failed. A sign-off over an error is a tool that
|
|
287
295
|
// did not read its own output.
|
|
288
|
-
const failed =
|
|
296
|
+
const failed = outcome.failures.length > 0;
|
|
289
297
|
if (theme.tier !== "contract" && !failed) {
|
|
290
298
|
out(`\n ${theme.dim(SIGN_OFF)}\n`);
|
|
291
299
|
}
|
|
@@ -496,9 +504,9 @@ function generateAdapters({ plan, harnesses, options, result, onProgress, onHarn
|
|
|
496
504
|
* person reading has just watched a bar fill. A re-run that wrote nothing says
|
|
497
505
|
* so plainly instead of inventing an achievement.
|
|
498
506
|
*/
|
|
499
|
-
function endingHeadline({ theme,
|
|
507
|
+
function endingHeadline({ theme, outcome, options }) {
|
|
500
508
|
const mark = theme.glyph;
|
|
501
|
-
const built
|
|
509
|
+
const { written, built, attention } = outcome;
|
|
502
510
|
|
|
503
511
|
// Something wants a human. Still ready — it is — but this is not the moment
|
|
504
512
|
// for confetti over somebody's conflicted file.
|
|
@@ -522,9 +530,9 @@ function endingHeadline({ theme, written, adapters, attention, options }) {
|
|
|
522
530
|
* so plainly instead of inventing an achievement out of the harnesses it did
|
|
523
531
|
* not have to configure.
|
|
524
532
|
*/
|
|
525
|
-
function endingDetail({
|
|
533
|
+
function endingDetail({ outcome, harnesses, options }) {
|
|
526
534
|
const parts = [];
|
|
527
|
-
const built
|
|
535
|
+
const { written, built, attention } = outcome;
|
|
528
536
|
|
|
529
537
|
if (written > 0) parts.push(`${written} file${plural(written)}`);
|
|
530
538
|
if (built > 0) parts.push(`${built} adapter${plural(built)}`);
|
|
@@ -987,15 +995,16 @@ export function formatFindings(findings, { theme = createTheme() } = {}) {
|
|
|
987
995
|
function report(args) {
|
|
988
996
|
// Two renderings, kept adjacent on purpose.
|
|
989
997
|
//
|
|
990
|
-
//
|
|
991
|
-
//
|
|
992
|
-
//
|
|
993
|
-
//
|
|
994
|
-
//
|
|
998
|
+
// What is reported is decided once, in `summarize`: both functions below read
|
|
999
|
+
// the same outcome and neither recounts anything. What remains separate is how
|
|
1000
|
+
// it looks. `contractReport` owes byte-for-byte what 1.4.1 printed, to scripts
|
|
1001
|
+
// that parse it; `expressiveReport` owes a person a legible hierarchy. Merging
|
|
1002
|
+
// them would mean one function whose every line carries a conditional, and the
|
|
1003
|
+
// first wording improvement would silently break somebody's grep.
|
|
995
1004
|
//
|
|
996
|
-
//
|
|
997
|
-
//
|
|
998
|
-
//
|
|
1005
|
+
// So a new reported fact belongs in the outcome, and reaches both renderings
|
|
1006
|
+
// from there; only its wording has to be chosen twice. They are written next
|
|
1007
|
+
// to each other so that editing one is an obvious prompt to consider the other.
|
|
999
1008
|
if (args.theme.tier === "contract") return contractReport(args);
|
|
1000
1009
|
return expressiveReport(args);
|
|
1001
1010
|
}
|
|
@@ -1007,7 +1016,7 @@ function report(args) {
|
|
|
1007
1016
|
* `test/non-interactive.test.mjs` and by a capture-and-compare against the
|
|
1008
1017
|
* published package, because a script somewhere is reading it.
|
|
1009
1018
|
*/
|
|
1010
|
-
function contractReport({
|
|
1019
|
+
function contractReport({ outcome, harnesses, customTools, cwd, gitRoot, options, out, err, theme }) {
|
|
1011
1020
|
const lines = [];
|
|
1012
1021
|
const verb = options.dryRun ? "Would install" : "Installed";
|
|
1013
1022
|
|
|
@@ -1017,25 +1026,24 @@ function contractReport({ result, plan, adapters, harnesses, customTools, cwd, g
|
|
|
1017
1026
|
}
|
|
1018
1027
|
lines.push("");
|
|
1019
1028
|
|
|
1020
|
-
const written
|
|
1029
|
+
const { written, overwritten, skipped } = outcome;
|
|
1021
1030
|
lines.push(` ${written} file${plural(written)} ${options.dryRun ? "to write" : "written"}`);
|
|
1022
1031
|
|
|
1023
|
-
if (
|
|
1024
|
-
lines.push(` ${
|
|
1032
|
+
if (overwritten > 0) {
|
|
1033
|
+
lines.push(` ${overwritten} file${plural(overwritten)} overwritten (--force)`);
|
|
1025
1034
|
}
|
|
1026
1035
|
|
|
1027
|
-
lines.push(...contractAdapterLines({
|
|
1036
|
+
lines.push(...contractAdapterLines({ outcome, options, theme }));
|
|
1028
1037
|
lines.push(...customToolLines(customTools));
|
|
1029
1038
|
|
|
1030
|
-
const skipped = plan.filter((item) => item.status === "skip");
|
|
1031
1039
|
if (skipped.length > 0) {
|
|
1032
1040
|
lines.push(` ${skipped.length} file${plural(skipped.length)} left untouched because they already exist:`);
|
|
1033
|
-
for (const
|
|
1041
|
+
for (const path of skipped) lines.push(` ${path}`);
|
|
1034
1042
|
lines.push("");
|
|
1035
1043
|
lines.push(" Nothing above was modified. Re-run with --force to replace them.");
|
|
1036
1044
|
}
|
|
1037
1045
|
|
|
1038
|
-
if (
|
|
1046
|
+
if (outcome.alreadyInstalled) {
|
|
1039
1047
|
lines.push("");
|
|
1040
1048
|
lines.push("The kit is already installed here.");
|
|
1041
1049
|
}
|
|
@@ -1053,7 +1061,7 @@ function contractReport({ result, plan, adapters, harnesses, customTools, cwd, g
|
|
|
1053
1061
|
|
|
1054
1062
|
out(lines.join("\n") + "\n");
|
|
1055
1063
|
|
|
1056
|
-
const failures =
|
|
1064
|
+
const { failures } = outcome;
|
|
1057
1065
|
if (failures.length > 0) {
|
|
1058
1066
|
const detail = failures.map((error) => ` ${error.relativePath}: ${error.message}`).join("\n");
|
|
1059
1067
|
err(`\ncreate-pathfinder: ${failures.length} file${plural(failures.length)} could not be written:\n${detail}\n`);
|
|
@@ -1197,10 +1205,12 @@ async function offerEditor({ editors, cwd, prompter, out, env, platform, theme }
|
|
|
1197
1205
|
* Empty when no harness was chosen, which is the default and must stay
|
|
1198
1206
|
* invisible: a scripted 1.4.1-era run prints exactly what it always did.
|
|
1199
1207
|
*/
|
|
1200
|
-
function contractAdapterLines({
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1208
|
+
function contractAdapterLines({ outcome, options, theme }) {
|
|
1209
|
+
// No explicit "no harness chosen" guard: that case is no rows and
|
|
1210
|
+
// `blocked: false`, so it falls straight through to an empty list. The
|
|
1211
|
+
// blocked case is checked first because it is *also* no rows, and the two
|
|
1212
|
+
// must not print the same nothing.
|
|
1213
|
+
if (outcome.blocked) {
|
|
1204
1214
|
return [
|
|
1205
1215
|
"",
|
|
1206
1216
|
" No adapters were generated, because the kit copy did not finish.",
|
|
@@ -1209,21 +1219,9 @@ function contractAdapterLines({ adapters, harnesses, options, theme }) {
|
|
|
1209
1219
|
];
|
|
1210
1220
|
}
|
|
1211
1221
|
|
|
1212
|
-
const failed = new Set(adapters.result.errors.map((error) => error.relativePath));
|
|
1213
1222
|
const lines = [];
|
|
1214
1223
|
|
|
1215
|
-
for (const harness of
|
|
1216
|
-
const mine = adapters.plan.filter(
|
|
1217
|
-
(item) => item.harness === harness && !failed.has(item.relativePath),
|
|
1218
|
-
);
|
|
1219
|
-
const count = (action) => mine.filter((item) => item.action === action).length;
|
|
1220
|
-
|
|
1221
|
-
const generated = count("write");
|
|
1222
|
-
const replaced = count("replace");
|
|
1223
|
-
const unchanged = count("up-to-date");
|
|
1224
|
-
const conflicts = mine.filter((item) => item.action === "conflict");
|
|
1225
|
-
const orphans = mine.filter((item) => item.action === "orphan");
|
|
1226
|
-
|
|
1224
|
+
for (const { harness, generated, replaced, unchanged, conflicts, orphans } of outcome.harnessRows) {
|
|
1227
1225
|
lines.push(
|
|
1228
1226
|
` ${generated} ${harness.label} skill adapter${plural(generated)} ` +
|
|
1229
1227
|
(options.dryRun ? "to generate" : "generated"),
|
|
@@ -1241,7 +1239,7 @@ function contractAdapterLines({ adapters, harnesses, options, theme }) {
|
|
|
1241
1239
|
lines.push(
|
|
1242
1240
|
` ${conflicts.length} file${plural(conflicts.length)} left untouched because Pathfinder did not write ${conflicts.length === 1 ? "it" : "them"}:`,
|
|
1243
1241
|
);
|
|
1244
|
-
for (const
|
|
1242
|
+
for (const path of conflicts) lines.push(` ${path}`);
|
|
1245
1243
|
lines.push("");
|
|
1246
1244
|
lines.push(
|
|
1247
1245
|
conflicts.length === 1
|
|
@@ -1252,8 +1250,8 @@ function contractAdapterLines({ adapters, harnesses, options, theme }) {
|
|
|
1252
1250
|
);
|
|
1253
1251
|
}
|
|
1254
1252
|
|
|
1255
|
-
for (const
|
|
1256
|
-
lines.push(` ${
|
|
1253
|
+
for (const path of orphans) {
|
|
1254
|
+
lines.push(` ${path} delegates to a skill this version no longer`);
|
|
1257
1255
|
lines.push(" ships. It was left in place; delete it yourself if you want it gone.");
|
|
1258
1256
|
}
|
|
1259
1257
|
}
|
|
@@ -1310,7 +1308,7 @@ function customToolLines(customTools = []) {
|
|
|
1310
1308
|
* and get colour. The paths underneath get neither, for the reason below.
|
|
1311
1309
|
* - **Diagnostics stay pasteable.** See `pathList`.
|
|
1312
1310
|
*/
|
|
1313
|
-
function expressiveReport({
|
|
1311
|
+
function expressiveReport({ outcome, harnesses, customTools, cwd, gitRoot, options, out, err, theme }) {
|
|
1314
1312
|
const mark = theme.glyph;
|
|
1315
1313
|
const lines = [];
|
|
1316
1314
|
const verb = options.dryRun ? "Would install" : "Installed";
|
|
@@ -1324,7 +1322,7 @@ function expressiveReport({ result, plan, adapters, harnesses, customTools, cwd,
|
|
|
1324
1322
|
);
|
|
1325
1323
|
}
|
|
1326
1324
|
|
|
1327
|
-
const written
|
|
1325
|
+
const { written, overwritten, skipped } = outcome;
|
|
1328
1326
|
// A zero is reported, never celebrated. `✓ 0 files written` is a tick over
|
|
1329
1327
|
// nothing happening, which is the kind of detail that makes a whole summary
|
|
1330
1328
|
// feel automated rather than read.
|
|
@@ -1341,18 +1339,17 @@ function expressiveReport({ result, plan, adapters, harnesses, customTools, cwd,
|
|
|
1341
1339
|
// was asked to do, and marking a requested action as a warning is how a tool
|
|
1342
1340
|
// teaches people to ignore its warnings. The files it replaced are still
|
|
1343
1341
|
// worth stating plainly, which is what `info` is for.
|
|
1344
|
-
if (
|
|
1342
|
+
if (overwritten > 0) {
|
|
1345
1343
|
lines.push(
|
|
1346
1344
|
railed(
|
|
1347
1345
|
theme,
|
|
1348
|
-
theme.info(`${mark.info} ${
|
|
1346
|
+
theme.info(`${mark.info} ${overwritten} file${plural(overwritten)} overwritten (--force)`),
|
|
1349
1347
|
),
|
|
1350
1348
|
);
|
|
1351
1349
|
}
|
|
1352
1350
|
|
|
1353
|
-
lines.push(...expressiveAdapterLines({
|
|
1351
|
+
lines.push(...expressiveAdapterLines({ outcome, options, theme }));
|
|
1354
1352
|
|
|
1355
|
-
const skipped = plan.filter((item) => item.status === "skip");
|
|
1356
1353
|
if (skipped.length > 0) {
|
|
1357
1354
|
lines.push(
|
|
1358
1355
|
railed(
|
|
@@ -1378,33 +1375,26 @@ function expressiveReport({ result, plan, adapters, harnesses, customTools, cwd,
|
|
|
1378
1375
|
theme,
|
|
1379
1376
|
word: "Skipped",
|
|
1380
1377
|
summary: `${skipped.length} file${plural(skipped.length)} already exist${skipped.length === 1 ? "s" : ""} and ${skipped.length === 1 ? "was" : "were"} left untouched`,
|
|
1381
|
-
paths: skipped
|
|
1378
|
+
paths: skipped,
|
|
1382
1379
|
advice: ["Nothing above was modified. Re-run with --force to replace them."],
|
|
1383
1380
|
}),
|
|
1384
1381
|
);
|
|
1385
1382
|
}
|
|
1386
1383
|
|
|
1387
|
-
lines.push(...expressiveAdapterBlocks({
|
|
1384
|
+
lines.push(...expressiveAdapterBlocks({ outcome, theme }));
|
|
1388
1385
|
|
|
1389
1386
|
if (customTools.length > 0) lines.push(...customToolLines(customTools));
|
|
1390
1387
|
|
|
1391
|
-
|
|
1392
|
-
//
|
|
1393
|
-
//
|
|
1394
|
-
|
|
1395
|
-
// thirty-six routine skips "things to look at" would turn the one number that
|
|
1396
|
-
// should mean something into noise nobody reads twice.
|
|
1397
|
-
const attention = adapters.blocked
|
|
1398
|
-
? 0
|
|
1399
|
-
: adapters.plan.filter((item) => item.action === "conflict" || item.action === "orphan").length;
|
|
1400
|
-
|
|
1401
|
-
if (failureCount === 0) {
|
|
1388
|
+
// `attention` is what actually wants a human: a contested path, or an adapter
|
|
1389
|
+
// pointing at a skill that is gone. Skipped files are deliberately not among
|
|
1390
|
+
// them — see `summarize`, which is where that decision now lives.
|
|
1391
|
+
if (outcome.failures.length === 0) {
|
|
1402
1392
|
lines.push("");
|
|
1403
1393
|
lines.push(
|
|
1404
1394
|
...readyBlock({
|
|
1405
1395
|
theme,
|
|
1406
|
-
headline: endingHeadline({ theme,
|
|
1407
|
-
detail: endingDetail({
|
|
1396
|
+
headline: endingHeadline({ theme, outcome, options }),
|
|
1397
|
+
detail: endingDetail({ outcome, harnesses, options }),
|
|
1408
1398
|
}),
|
|
1409
1399
|
);
|
|
1410
1400
|
}
|
|
@@ -1430,7 +1420,7 @@ function expressiveReport({ result, plan, adapters, harnesses, customTools, cwd,
|
|
|
1430
1420
|
|
|
1431
1421
|
out(lines.join("\n") + "\n");
|
|
1432
1422
|
|
|
1433
|
-
const failures =
|
|
1423
|
+
const { failures } = outcome;
|
|
1434
1424
|
if (failures.length > 0) {
|
|
1435
1425
|
// `bad`, not `warn`, and the distinction is the whole point of having both:
|
|
1436
1426
|
// everything above is an outcome somebody may want to know about, and this
|
|
@@ -1480,29 +1470,21 @@ function pathList(paths) {
|
|
|
1480
1470
|
}
|
|
1481
1471
|
|
|
1482
1472
|
/** The per-harness summary counts, on the gutter, each at its own severity. */
|
|
1483
|
-
function expressiveAdapterLines({
|
|
1473
|
+
function expressiveAdapterLines({ outcome, options, theme }) {
|
|
1484
1474
|
const mark = theme.glyph;
|
|
1485
|
-
if (harnesses.length === 0) return [];
|
|
1486
1475
|
|
|
1487
|
-
|
|
1476
|
+
// No "no harness chosen" guard: that case is no rows and `blocked: false`,
|
|
1477
|
+
// so it falls through to an empty list. Blocked is checked first because it
|
|
1478
|
+
// is also no rows, and the two must not print the same nothing.
|
|
1479
|
+
if (outcome.blocked) {
|
|
1488
1480
|
return [
|
|
1489
1481
|
railed(theme, theme.bad(`${mark.bad} No adapters were generated`) + theme.dim(" (the kit copy did not finish)")),
|
|
1490
1482
|
];
|
|
1491
1483
|
}
|
|
1492
1484
|
|
|
1493
|
-
const failed = new Set(adapters.result.errors.map((error) => error.relativePath));
|
|
1494
1485
|
const lines = [];
|
|
1495
1486
|
|
|
1496
|
-
for (const harness of
|
|
1497
|
-
const mine = adapters.plan.filter(
|
|
1498
|
-
(item) => item.harness === harness && !failed.has(item.relativePath),
|
|
1499
|
-
);
|
|
1500
|
-
const count = (action) => mine.filter((item) => item.action === action).length;
|
|
1501
|
-
|
|
1502
|
-
const generated = count("write");
|
|
1503
|
-
const replaced = count("replace");
|
|
1504
|
-
const unchanged = count("up-to-date");
|
|
1505
|
-
|
|
1487
|
+
for (const { harness, generated, replaced, unchanged, conflicts, orphans } of outcome.harnessRows) {
|
|
1506
1488
|
lines.push(
|
|
1507
1489
|
railed(
|
|
1508
1490
|
theme,
|
|
@@ -1525,9 +1507,6 @@ function expressiveAdapterLines({ adapters, harnesses, options, theme }) {
|
|
|
1525
1507
|
);
|
|
1526
1508
|
}
|
|
1527
1509
|
|
|
1528
|
-
const conflicts = mine.filter((item) => item.action === "conflict");
|
|
1529
|
-
const orphans = mine.filter((item) => item.action === "orphan");
|
|
1530
|
-
|
|
1531
1510
|
if (conflicts.length > 0) {
|
|
1532
1511
|
lines.push(
|
|
1533
1512
|
railed(
|
|
@@ -1553,20 +1532,12 @@ function expressiveAdapterLines({ adapters, harnesses, options, theme }) {
|
|
|
1553
1532
|
}
|
|
1554
1533
|
|
|
1555
1534
|
/** The conflict and orphan detail blocks, with their paths kept pasteable. */
|
|
1556
|
-
function expressiveAdapterBlocks({
|
|
1557
|
-
if (
|
|
1535
|
+
function expressiveAdapterBlocks({ outcome, theme }) {
|
|
1536
|
+
if (outcome.blocked) return [];
|
|
1558
1537
|
|
|
1559
|
-
const failed = new Set(adapters.result.errors.map((error) => error.relativePath));
|
|
1560
1538
|
const blocks = [];
|
|
1561
1539
|
|
|
1562
|
-
for (const harness of
|
|
1563
|
-
const mine = adapters.plan.filter(
|
|
1564
|
-
(item) => item.harness === harness && !failed.has(item.relativePath),
|
|
1565
|
-
);
|
|
1566
|
-
|
|
1567
|
-
const conflicts = mine.filter((item) => item.action === "conflict");
|
|
1568
|
-
const orphans = mine.filter((item) => item.action === "orphan");
|
|
1569
|
-
|
|
1540
|
+
for (const { harness, conflicts, orphans } of outcome.harnessRows) {
|
|
1570
1541
|
if (conflicts.length > 0) {
|
|
1571
1542
|
const one = conflicts.length === 1;
|
|
1572
1543
|
blocks.push(
|
|
@@ -1574,7 +1545,7 @@ function expressiveAdapterBlocks({ adapters, harnesses, theme }) {
|
|
|
1574
1545
|
theme,
|
|
1575
1546
|
word: "Conflict",
|
|
1576
1547
|
summary: `${conflicts.length} ${harness.label} file${plural(conflicts.length)} at ${one ? "a path an adapter wants" : "paths adapters want"}, which Pathfinder did not write`,
|
|
1577
|
-
paths: conflicts
|
|
1548
|
+
paths: conflicts,
|
|
1578
1549
|
advice: [
|
|
1579
1550
|
`Re-run with --force to replace ${one ? "it" : "them"} ${theme.glyph.dash} note that --force also`,
|
|
1580
1551
|
"overwrites Pathfinder kit files you have edited.",
|
|
@@ -1590,7 +1561,7 @@ function expressiveAdapterBlocks({ adapters, harnesses, theme }) {
|
|
|
1590
1561
|
theme,
|
|
1591
1562
|
word: "Orphan",
|
|
1592
1563
|
summary: `${orphans.length} ${harness.label} adapter${plural(orphans.length)} delegat${one ? "es" : "e"} to a skill this version no longer ships`,
|
|
1593
|
-
paths: orphans
|
|
1564
|
+
paths: orphans,
|
|
1594
1565
|
advice: [
|
|
1595
1566
|
`Left in place. Delete ${one ? "it" : "them"} yourself if you want ${one ? "it" : "them"} gone.`,
|
|
1596
1567
|
],
|
package/src/install.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { dirname, join, relative, sep } from "node:path";
|
|
18
18
|
|
|
19
|
-
import { COPY_LIST, isExcluded } from "./kit.mjs";
|
|
19
|
+
import { COPY_LIST, isExcluded, neverShips } from "./kit.mjs";
|
|
20
20
|
import {
|
|
21
21
|
ADAPTER_STATE,
|
|
22
22
|
adapterPath,
|
|
@@ -43,6 +43,10 @@ export function planInstall(kitRoot, targetRoot, { force = false } = {}) {
|
|
|
43
43
|
for (const entry of COPY_LIST) {
|
|
44
44
|
for (const source of walkFiles(join(kitRoot, entry))) {
|
|
45
45
|
const relativePath = relative(kitRoot, source).split(sep).join("/");
|
|
46
|
+
// Filtered here rather than in walkFiles, which sees basenames only and
|
|
47
|
+
// would have to guess whether a `tracker.md` is *the* one.
|
|
48
|
+
if (neverShips(relativePath)) continue;
|
|
49
|
+
|
|
46
50
|
const destination = join(targetRoot, relativePath);
|
|
47
51
|
const exists = existsSync(destination);
|
|
48
52
|
|