@damiangilgonzalez/harness 1.3.13 → 1.3.15
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 +15 -9
- package/manifest.json +60 -4
- package/package.json +1 -1
- package/scripts/doctor.js +13 -4
- package/scripts/install-apply.js +73 -9
- package/scripts/lib/backups.js +13 -0
- package/scripts/lib/default-skill-floor.js +14 -0
- package/scripts/lib/domains.js +1 -1
- package/scripts/lib/onboarding-apply.js +73 -12
- package/scripts/lib/onboarding.js +8 -0
- package/scripts/lib/repair.js +8 -4
- package/scripts/lib/review-findings.js +175 -0
- package/scripts/rsc.js +51 -3
- package/skills/code-review/SKILL.md +17 -1
- package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +9 -9
- package/skills/domain-modeling/SKILL.md +14 -12
- package/skills/harness/SKILL.md +6 -0
- package/skills/harness/references/wiki-protocol.md +5 -0
- package/skills/implement-spec/SKILL.md +43 -0
- package/skills/implement-spec/evals/cases.yaml +33 -0
- package/skills/init/references/accompaniment-and-profile.md +1 -1
- package/skills/orient/SKILL.md +9 -1
- package/skills/orient/references/orientation-contract.md +1 -1
- package/skills/pr/CREDITS.md +3 -0
- package/skills/pr/SKILL.md +173 -0
- package/skills/pr/evals/cases.yaml +33 -0
- package/skills/retro/SKILL.md +47 -0
- package/skills/retro/evals/cases.yaml +32 -0
- package/skills/suggest/SKILL.md +1 -1
- package/skills/wait-what/SKILL.md +1 -1
- package/targets/_md-block.js +1 -1
- package/targets/agents.js +37 -1
- package/targets/claude.js +25 -16
- package/targets/hook-once.mjs +6 -3
- package/targets/memory.js +1 -1
- package/targets/sello.mjs +2 -2
- package/targets/session-memory-core.mjs +1 -1
- package/targets/session-start.mjs +2 -2
- package/targets/ship-guard.mjs +1 -1
- package/targets/worktree-reaper.mjs +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/@damiangilgonzalez/harness)
|
|
10
10
|
[](https://www.npmjs.com/package/@damiangilgonzalez/harness)
|
|
11
|
-
[](#the-catalog)
|
|
12
12
|
[](LICENSE)
|
|
13
13
|
[](https://github.com/ericrisco/rsc-harness/stargazers)
|
|
14
14
|
|
|
@@ -73,6 +73,9 @@ accompaniment, project kind and goal **before it writes anything**, reads only t
|
|
|
73
73
|
root, and shows every selected or deferred skill, agent and hook with its reason. Relay those
|
|
74
74
|
questions to the user; never invent or default their answers. The first pass writes nothing and
|
|
75
75
|
prints a SHA-256 plan id. Only rerun with `--accept-plan <id>` after the user accepts that exact plan.
|
|
76
|
+
If the project already has a skill of its own with a catalog name, the plan lists it under
|
|
77
|
+
**Your own skills this plan replaces** — tell the user before accepting; renaming their folder and
|
|
78
|
+
regenerating the plan keeps theirs.
|
|
76
79
|
|
|
77
80
|
For a non-interactive agent, collect the answers and preview the same plan explicitly:
|
|
78
81
|
|
|
@@ -202,7 +205,7 @@ $ rsc onboard
|
|
|
202
205
|
██████╗ ███████╗ ██████╗ ← animated gradient wordmark
|
|
203
206
|
██╔══██╗██╔════╝██╔════╝
|
|
204
207
|
██████╔╝███████╗██║
|
|
205
|
-
|
|
208
|
+
294 skills · one CLI · zero bloat
|
|
206
209
|
|
|
207
210
|
How technical should the conversation be?
|
|
208
211
|
How much accompaniment do you want?
|
|
@@ -241,13 +244,13 @@ rsc add youtube-api remotion-video # …grow a channel, edit with Remotion
|
|
|
241
244
|
rsc add fastapi --target claude,codex # install into several assistants at once
|
|
242
245
|
rsc install --profile minimal # the base: orient + suggest + bro + unslop + show-me + eli5 + harness + init + teach
|
|
243
246
|
rsc install --profile core # floor + the full SDD workflow
|
|
244
|
-
rsc install --profile full # everything (all
|
|
247
|
+
rsc install --profile full # everything (all 294 skills)
|
|
245
248
|
rsc install --profile full --without go
|
|
246
249
|
rsc consult "I want to launch a SaaS" # recommend only, no install
|
|
247
250
|
rsc registry refresh # write .rsc/skill-registry.{json,md}
|
|
248
251
|
rsc list # installed skills, agents and commands
|
|
249
252
|
rsc capabilities # installed/available surfaces + memory mode
|
|
250
|
-
rsc doctor # health, missing backing, hooks and local memory
|
|
253
|
+
rsc doctor # health, missing backing, hooks and local memory; exits 1 when unhealthy
|
|
251
254
|
rsc memory status # full / assisted / unsupported / degraded
|
|
252
255
|
rsc memory save --session handoff # force a deterministic local checkpoint
|
|
253
256
|
rsc memory resume # print this branch/worktree continuation
|
|
@@ -405,7 +408,7 @@ just asks in plain language.
|
|
|
405
408
|
|
|
406
409
|
## The catalog
|
|
407
410
|
|
|
408
|
-
|
|
411
|
+
294 skills, grouped by what you're trying to do. Click any skill to read its
|
|
409
412
|
`SKILL.md`. It fires on its own when a task matches.
|
|
410
413
|
|
|
411
414
|
### 🧭 Core & control plane
|
|
@@ -584,7 +587,8 @@ inside `grill-with-docs`.
|
|
|
584
587
|
|
|
585
588
|
| Skill | What it does |
|
|
586
589
|
|---|---|
|
|
587
|
-
| [implement](skills/implement/) | Builds
|
|
590
|
+
| [implement-spec](skills/implement-spec/) | Builds a whole spec from its `to-tickets` graph: parallel subagents in worktrees, one integration branch, `code-review` at the end. |
|
|
591
|
+
| [implement](skills/implement/) | Builds one loose ticket from `to-tickets`. Ordinary work runs `superpowers:executing-plans` instead. |
|
|
588
592
|
|
|
589
593
|
#### 3 · Interface
|
|
590
594
|
|
|
@@ -618,14 +622,15 @@ When the work touches a screen. Sixteen overlapping skills; pick the one that ma
|
|
|
618
622
|
|
|
619
623
|
| The work | Order |
|
|
620
624
|
|---|---|
|
|
621
|
-
| Did not touch a screen | `code-review` |
|
|
622
|
-
| Touched a screen | `revision-de-cambios` → `revision-interfaz` → `code-review` |
|
|
625
|
+
| Did not touch a screen | `code-review` → `pr` |
|
|
626
|
+
| Touched a screen | `revision-de-cambios` → `revision-interfaz` → `code-review` → `pr` |
|
|
623
627
|
|
|
624
628
|
| Skill | What it does |
|
|
625
629
|
|---|---|
|
|
626
630
|
| [code-review](skills/code-review/) | Two axes in parallel: the repo's standards and the spec. |
|
|
627
631
|
| [revision-de-cambios](skills/revision-de-cambios/) | Reviews the change, reads the removed lines and classifies each finding. Only if it touched a screen. |
|
|
628
632
|
| [revision-interfaz](skills/revision-interfaz/) | Runs the six `mejor-*` skills as one report with a verdict. Only if it touched a screen. |
|
|
633
|
+
| [pr](skills/pr/) | Writes the PR body: a visual summary, before/after evidence, and the merge danger (one-way or two-way door, blast radius). |
|
|
629
634
|
| [handoff](skills/handoff/) | Compacts the session into a handoff document. Optional. |
|
|
630
635
|
|
|
631
636
|
#### 9 · Other
|
|
@@ -635,6 +640,7 @@ When the work touches a screen. Sixteen overlapping skills; pick the one that ma
|
|
|
635
640
|
| [teach](skills/teach/) | Teaching across sessions. |
|
|
636
641
|
| [muscle-memory](skills/muscle-memory/) | A Python kata gym. |
|
|
637
642
|
| [wait-what](skills/wait-what/) | Handbrake: stop and restate. |
|
|
643
|
+
| [retro](skills/retro/) | Retrospective on a session: what to change in the agent's environment. You approve every change. |
|
|
638
644
|
| [writing-for-agents](skills/writing-for-agents/) | How to write documents an agent consumes. |
|
|
639
645
|
|
|
640
646
|
---
|
|
@@ -682,7 +688,7 @@ The richer surfaces are intentionally narrower than skill support:
|
|
|
682
688
|
| Windsurf, Cline, Roo | unsupported | yes | unsupported |
|
|
683
689
|
| Antigravity, Zed, Continue, Amp, Jules, Aider | unsupported | unsupported | unsupported |
|
|
684
690
|
|
|
685
|
-
`manifest.json` is the generated public inventory:
|
|
691
|
+
`manifest.json` is the generated public inventory: 34 agents (5 base + 29
|
|
686
692
|
selective specialists) and 43 command entries (10 fixed + 33 stack aliases).
|
|
687
693
|
Unsupported means rsc writes nothing for that surface; it does not emulate a
|
|
688
694
|
provider feature with an unverified file.
|
package/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.3.
|
|
2
|
+
"version": "1.3.15",
|
|
3
3
|
"counts": {
|
|
4
|
-
"skills":
|
|
5
|
-
"agents":
|
|
4
|
+
"skills": 294,
|
|
5
|
+
"agents": 34,
|
|
6
6
|
"commands": 43,
|
|
7
7
|
"fixedCommands": 10
|
|
8
8
|
},
|
|
@@ -1395,7 +1395,7 @@
|
|
|
1395
1395
|
},
|
|
1396
1396
|
{
|
|
1397
1397
|
"id": "domain-modeling",
|
|
1398
|
-
"description": "Build and
|
|
1398
|
+
"description": "Build and sharpen the project's domain model. Use when discussing codebase terminology, writing or editing a GLOSSARY.md, or recording or editing an ADR.",
|
|
1399
1399
|
"tags": [
|
|
1400
1400
|
"domain-modeling",
|
|
1401
1401
|
"glossary",
|
|
@@ -2467,6 +2467,23 @@
|
|
|
2467
2467
|
"full"
|
|
2468
2468
|
]
|
|
2469
2469
|
},
|
|
2470
|
+
{
|
|
2471
|
+
"id": "implement-spec",
|
|
2472
|
+
"description": "Implement the result of /to-spec and /to-tickets in code, orchestrating parallel implementer subagents over the ticket graph.",
|
|
2473
|
+
"tags": [
|
|
2474
|
+
"implementation",
|
|
2475
|
+
"spec",
|
|
2476
|
+
"tickets",
|
|
2477
|
+
"subagents",
|
|
2478
|
+
"orchestration"
|
|
2479
|
+
],
|
|
2480
|
+
"recommends": [],
|
|
2481
|
+
"profiles": [
|
|
2482
|
+
"core",
|
|
2483
|
+
"ui",
|
|
2484
|
+
"full"
|
|
2485
|
+
]
|
|
2486
|
+
},
|
|
2470
2487
|
{
|
|
2471
2488
|
"id": "ingeniero-diseno-web",
|
|
2472
2489
|
"description": "Build or redesign polished visual artifacts that render in a browser with HTML/CSS/JavaScript/React — pages, dashboards, prototypes, presentations, animations, UI mockups, and data visualizations. Use for visual front-end creation, design-system exploration, design critique, or explicit browser-based acceptance/QA. Not for back-end, CLI, non-visual code, or converting sources into long-form articles.",
|
|
@@ -3786,6 +3803,22 @@
|
|
|
3786
3803
|
"full"
|
|
3787
3804
|
]
|
|
3788
3805
|
},
|
|
3806
|
+
{
|
|
3807
|
+
"id": "pr",
|
|
3808
|
+
"description": "Use when writing a PR body: a visual summary, before/after evidence, and the merge danger (one-way or two-way door, blast radius).",
|
|
3809
|
+
"tags": [
|
|
3810
|
+
"pull-request",
|
|
3811
|
+
"review",
|
|
3812
|
+
"evidence",
|
|
3813
|
+
"risk"
|
|
3814
|
+
],
|
|
3815
|
+
"recommends": [],
|
|
3816
|
+
"profiles": [
|
|
3817
|
+
"core",
|
|
3818
|
+
"ui",
|
|
3819
|
+
"full"
|
|
3820
|
+
]
|
|
3821
|
+
},
|
|
3789
3822
|
{
|
|
3790
3823
|
"id": "presentations",
|
|
3791
3824
|
"description": "Use when building, theming, or exporting a presentation deck — pitch, sales, keynote, board/QBR, leave-behind one-pager — from slide structure to a token-based theme to PDF or editable PPTX (Marp, Slidev, python-pptx). NOT the words (that is `marketing`), NOT the visual tokens (that is `design`), NOT the investor story arc (that is `pitch-deck`).",
|
|
@@ -4288,6 +4321,21 @@
|
|
|
4288
4321
|
],
|
|
4289
4322
|
"profiles": []
|
|
4290
4323
|
},
|
|
4324
|
+
{
|
|
4325
|
+
"id": "retro",
|
|
4326
|
+
"description": "Conduct a retrospective on a coding session: find what in the agent's environment (navigation, checks, standards, steering files, tooling) should change so future runs go better.",
|
|
4327
|
+
"tags": [
|
|
4328
|
+
"retrospective",
|
|
4329
|
+
"session-review",
|
|
4330
|
+
"agent-environment"
|
|
4331
|
+
],
|
|
4332
|
+
"recommends": [],
|
|
4333
|
+
"profiles": [
|
|
4334
|
+
"core",
|
|
4335
|
+
"ui",
|
|
4336
|
+
"full"
|
|
4337
|
+
]
|
|
4338
|
+
},
|
|
4291
4339
|
{
|
|
4292
4340
|
"id": "review-management",
|
|
4293
4341
|
"description": "Use when a public review needs an on-voice reply, when you need to earn more reviews legally, or when an aggregate rating is slipping across Google Business Profile, Trustpilot, the App Store or Google Play — including deciding reply versus flag, getting an unfair one-star removed rather than merely answered, and wiring programmatic replies. NOT a private support ticket against an SLA (that is `customer-support`), NOT churn, NPS or save plays (that is `retention`).",
|
|
@@ -5988,6 +6036,14 @@
|
|
|
5988
6036
|
"fastapi"
|
|
5989
6037
|
]
|
|
5990
6038
|
},
|
|
6039
|
+
{
|
|
6040
|
+
"id": "finding-verifier",
|
|
6041
|
+
"description": "Adversarial verifier of review findings: takes ONE consolidated finding and tries to prove it false against the code. Default stance: the finding is false; only a reproduced or traced failure is CONFIRMED. Fresh context, edits nothing.",
|
|
6042
|
+
"role": "refuter",
|
|
6043
|
+
"tier": "balanced",
|
|
6044
|
+
"tools": null,
|
|
6045
|
+
"skills": []
|
|
6046
|
+
},
|
|
5991
6047
|
{
|
|
5992
6048
|
"id": "flutter-build-resolver",
|
|
5993
6049
|
"description": "Flutter build resolver: reproduces a failing build and applies the minimum verified repair. NOT widget review or package modernization.",
|
package/package.json
CHANGED
package/scripts/doctor.js
CHANGED
|
@@ -65,6 +65,10 @@ function selloStatus(root) {
|
|
|
65
65
|
//
|
|
66
66
|
// Hookless targets get an empty list by construction: their always-on surface is a
|
|
67
67
|
// markdown block with no script behind it, so there is nothing that can go missing.
|
|
68
|
+
// A quoted span counts as a path only if it starts like one and holds no shell operator: a whole
|
|
69
|
+
// `sh -c '…'` snippet is not a path, and reading it as one reports a present script missing.
|
|
70
|
+
const SCRIPT_RE =
|
|
71
|
+
/"((?:[A-Za-z]:)?[\\/~][^"&;|]*[\\/]\.rsc[\\/][^"\s]+?\.mjs)"|'((?:[A-Za-z]:)?[\\/~][^'&;|]*[\\/]\.rsc[\\/][^'\s]+?\.mjs)'|([^"'\s]*[\\/]\.rsc[\\/][^"'\s]+\.mjs)/g;
|
|
68
72
|
export function missingHookScripts({ target, home = homedir(), cwd = process.cwd() } = {}) {
|
|
69
73
|
if (HOOKLESS_TARGETS.has(target)) return [];
|
|
70
74
|
const file = targetPaths(target, home, cwd).hookTarget;
|
|
@@ -84,8 +88,10 @@ export function missingHookScripts({ target, home = homedir(), cwd = process.cwd
|
|
|
84
88
|
const expand = (c) => c.split('${CLAUDE_PROJECT_DIR}').join(cwd);
|
|
85
89
|
for (const raw of commands) {
|
|
86
90
|
const cmd = expand(raw);
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
// Quoted forms first: a project path may hold a space, and stopping at whitespace kept only
|
|
92
|
+
// the tail (`entreno/.rsc/x.mjs`), which then resolved against the cwd and read as missing (#283).
|
|
93
|
+
for (const m of cmd.matchAll(SCRIPT_RE)) {
|
|
94
|
+
const script = m[1] ?? m[2] ?? m[3];
|
|
89
95
|
if (!existsSync(script)) seen.add(script);
|
|
90
96
|
}
|
|
91
97
|
}
|
|
@@ -202,7 +208,7 @@ export function doctor({ target, home, cwd }) {
|
|
|
202
208
|
const missingAgents = (state.agents || []).filter((id) => {
|
|
203
209
|
const path = agentPath(target, root, id);
|
|
204
210
|
return path && !existsSync(path);
|
|
205
|
-
}).map((id) => ({ id, action: 'Run `npx @damiangilgonzalez/harness sync` to restore this managed agent.' }));
|
|
211
|
+
}).map((id) => ({ id, path: agentPath(target, root, id), action: 'Run `npx @damiangilgonzalez/harness sync` to restore this managed agent.' }));
|
|
206
212
|
const report = {
|
|
207
213
|
target,
|
|
208
214
|
installed: Object.keys(state.skills),
|
|
@@ -252,9 +258,12 @@ export function doctor({ target, home, cwd }) {
|
|
|
252
258
|
commandOrphans,
|
|
253
259
|
commandCollisions: state.commandCollisions || [],
|
|
254
260
|
};
|
|
261
|
+
// #277 — the three "missing" lists share one shape, { id, path, action }, so whoever builds on top
|
|
262
|
+
// of doctor reads them the same way; and `healthy` is what the exit code says.
|
|
255
263
|
for (const [id, e] of Object.entries(state.skills)) {
|
|
256
|
-
for (const f of e.files) if (!existsSync(f)) report.missing.push(
|
|
264
|
+
for (const f of e.files) if (!existsSync(f)) report.missing.push({ id, path: f, action: 'Run `npx @ericrisco/rsc sync` to restore this managed skill.' });
|
|
257
265
|
}
|
|
266
|
+
report.healthy = report.hookWired && !report.missing.length && !missingAgents.length && !missingCommands.length;
|
|
258
267
|
return report;
|
|
259
268
|
}
|
|
260
269
|
|
package/scripts/install-apply.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from '../targets/agents.js';
|
|
13
13
|
import { projectOptOuts } from '../targets/opt-outs.js';
|
|
14
14
|
import { readState, writeState } from './lib/state.js';
|
|
15
|
-
import { withDefaultSkillFloor } from './lib/default-skill-floor.js';
|
|
15
|
+
import { withCodeFlowFloor, withDefaultSkillFloor } from './lib/default-skill-floor.js';
|
|
16
16
|
import { readManifest, writeManifest } from './lib/manifest-file.js';
|
|
17
17
|
import { createBackup } from './lib/backups.js';
|
|
18
18
|
import {
|
|
@@ -78,9 +78,12 @@ export function generatedHookFiles({ target, cwd, policy }) {
|
|
|
78
78
|
join(cwd, '.rsc', 'hook-once.mjs'),
|
|
79
79
|
join(cwd, '.rsc', 'worktree-reaper.mjs'),
|
|
80
80
|
];
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
// The danger guard is declared on its own terms (#273): present unless the plan says otherwise,
|
|
82
|
+
// whether or not the code guards are.
|
|
83
|
+
const danger = policy?.dangerGuard === false ? [] : [join(cwd, '.rsc', 'danger-guard.mjs')];
|
|
84
|
+
if (policy?.codeHooks === false) return [...lifecycle, ...danger, join(cwd, '.rsc', 'suggest-always-on.md')];
|
|
85
|
+
return [...lifecycle, ...danger,
|
|
86
|
+
join(cwd, '.rsc', 'ship-guard.mjs'),
|
|
84
87
|
join(cwd, '.rsc', 'gitmoji-guard.mjs'), join(cwd, '.rsc', 'userprompt-gate.mjs'),
|
|
85
88
|
join(cwd, '.rsc', 'sello.mjs')];
|
|
86
89
|
}
|
|
@@ -279,6 +282,7 @@ export async function applyInstall({ skillIds = [], agentIds = [], target, home,
|
|
|
279
282
|
baseAgents: policy.baseAgents !== false,
|
|
280
283
|
alwaysOn: policy.alwaysOn !== false,
|
|
281
284
|
codeHooks: policy.codeHooks !== false,
|
|
285
|
+
dangerGuard: policy.dangerGuard !== false,
|
|
282
286
|
gitmojiGuard: policy.gitmojiGuard !== false,
|
|
283
287
|
memory: policy.memory !== false,
|
|
284
288
|
context7: policy.context7 === true,
|
|
@@ -396,12 +400,24 @@ export function ignoreLocalState(cwd = process.cwd(), target) {
|
|
|
396
400
|
}
|
|
397
401
|
|
|
398
402
|
const add = wanted.filter((w) => !present.has(norm(w)));
|
|
399
|
-
|
|
403
|
+
// Every append used to open a new block with its own header, so a project synced across a dozen
|
|
404
|
+
// versions carried a dozen copies of it. Adjacent rsc blocks are one block: fold them (only our
|
|
405
|
+
// header lines and the blank seams between our blocks go; every entry stays, in order).
|
|
406
|
+
const folded = foldIgnoreBlocks(text);
|
|
407
|
+
if (!add.length) {
|
|
408
|
+
if (folded === text) return null;
|
|
409
|
+
try { writeFileSync(gi, folded); return gi; } catch { return null; }
|
|
410
|
+
}
|
|
400
411
|
|
|
401
|
-
//
|
|
402
|
-
|
|
412
|
+
// Never reorder, never touch a line of theirs. When the file already ends with our block, the new
|
|
413
|
+
// entries join it; otherwise they open one.
|
|
414
|
+
const base = folded === '' || folded.endsWith('\n') ? folded : `${folded}\n`;
|
|
415
|
+
const next = endsWithIgnoreBlock(base)
|
|
416
|
+
? `${base}${add.join('\n')}\n`
|
|
417
|
+
: `${base}${base === '' ? '' : '\n'}${RSC_IGNORE_HEADER}\n${add.join('\n')}\n`;
|
|
403
418
|
try {
|
|
404
|
-
|
|
419
|
+
if (folded === text && next.startsWith(text)) appendFileSync(gi, next.slice(text.length));
|
|
420
|
+
else writeFileSync(gi, next);
|
|
405
421
|
if (target) {
|
|
406
422
|
const statePath = targetPaths(target, undefined, cwd).stateFile;
|
|
407
423
|
const state = readState(statePath);
|
|
@@ -418,6 +434,14 @@ export function collisions({ cwd = process.cwd(), target, home, skillIds = [] })
|
|
|
418
434
|
return skillIds.filter((id) => !managed.has(id) && existsSync(paths.skillDir(id)));
|
|
419
435
|
}
|
|
420
436
|
|
|
437
|
+
// The same test as `collisions`, across every target of a plan, as the paths the user would recognise.
|
|
438
|
+
// `onboard` puts these in the plan itself, so the id the user accepts is an id that names them.
|
|
439
|
+
export function ownSkillPaths({ cwd = process.cwd(), targets = [], skillIds = [], home }) {
|
|
440
|
+
const found = targets.flatMap((target) => collisions({ cwd, target, home, skillIds })
|
|
441
|
+
.map((id) => relative(cwd, targetPaths(target, home, cwd).skillDir(id)).split(sep).join('/')));
|
|
442
|
+
return [...new Set(found)].sort();
|
|
443
|
+
}
|
|
444
|
+
|
|
421
445
|
export function listInstalled({ target, home, cwd = process.cwd() }) {
|
|
422
446
|
const paths = targetPaths(target, home, cwd);
|
|
423
447
|
return Object.keys(readState(paths.stateFile).skills);
|
|
@@ -602,7 +626,10 @@ export async function syncInstalled({ target, home, cwd = process.cwd(), dryRun
|
|
|
602
626
|
// mandatory never reaches an existing harness: `add` and `install` apply the floor, sync did
|
|
603
627
|
// not, and sync is the only one an upgrade runs. Applied only to a harness that already declares
|
|
604
628
|
// something: a directory declaring nothing is not a harness missing its floor.
|
|
605
|
-
|
|
629
|
+
// Lo mismo con el flujo del gate: si el gate está conectado, sus skills llegan con el update.
|
|
630
|
+
const declared = declaredRaw.length
|
|
631
|
+
? withCodeFlowFloor(withDefaultSkillFloor(declaredRaw), manifest?.onboarding?.plan?.policy)
|
|
632
|
+
: declaredRaw;
|
|
606
633
|
const declaredAgents = state.explicitAgents?.length ? state.explicitAgents : (manifest?.agents || []);
|
|
607
634
|
if (!declared.length && !declaredAgents.length) return dryRun ? { dryRun: true, synced: [], syncedAgents: [], paths: [] } : { synced: [], syncedAgents: [], backup: null };
|
|
608
635
|
if (dryRun) {
|
|
@@ -632,6 +659,43 @@ export async function syncInstalled({ target, home, cwd = process.cwd(), dryRun
|
|
|
632
659
|
// own knowledge — kept unless `withDocs` is set. Returns the paths touched.
|
|
633
660
|
// Note: backups live under `.rsc/backups/`, which this removes — so purge does not
|
|
634
661
|
// snapshot (a pre-purge backup would delete itself). It is the deliberate escape hatch.
|
|
662
|
+
const RSC_IGNORE_HEADER = '# rsc local state (hooks, seals, logs) and managed skill links — machine-local';
|
|
663
|
+
|
|
664
|
+
// Merge consecutive rsc blocks into the first: drop a repeated header and the blank seam before it
|
|
665
|
+
// when the block above was ours too. A block of the user's in between ends ours, so nothing of
|
|
666
|
+
// theirs is ever moved or absorbed.
|
|
667
|
+
export function foldIgnoreBlocks(text) {
|
|
668
|
+
const out = [];
|
|
669
|
+
let ours = false;
|
|
670
|
+
let afterBlank = false;
|
|
671
|
+
for (const line of text.split('\n')) {
|
|
672
|
+
if (line === RSC_IGNORE_HEADER) {
|
|
673
|
+
if (ours && afterBlank) {
|
|
674
|
+
while (out.length && out[out.length - 1].trim() === '') out.pop();
|
|
675
|
+
afterBlank = false;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
ours = true;
|
|
679
|
+
} else if (line.trim() === '') {
|
|
680
|
+
afterBlank = true;
|
|
681
|
+
out.push(line);
|
|
682
|
+
continue;
|
|
683
|
+
} else if (afterBlank) {
|
|
684
|
+
ours = false;
|
|
685
|
+
}
|
|
686
|
+
afterBlank = false;
|
|
687
|
+
out.push(line);
|
|
688
|
+
}
|
|
689
|
+
return out.join('\n');
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function endsWithIgnoreBlock(text) {
|
|
693
|
+
const lines = text.replace(/\n+$/, '').split('\n');
|
|
694
|
+
let i = lines.length - 1;
|
|
695
|
+
while (i > 0 && lines[i - 1].trim() !== '') i -= 1;
|
|
696
|
+
return lines[i] === RSC_IGNORE_HEADER;
|
|
697
|
+
}
|
|
698
|
+
|
|
635
699
|
export async function purge({ home, cwd = process.cwd(), withDocs = false, dryRun = false } = {}) {
|
|
636
700
|
const removed = [];
|
|
637
701
|
const drop = (p, recursive = false) => {
|
package/scripts/lib/backups.js
CHANGED
|
@@ -40,6 +40,19 @@ export function createBackup({ cwd = process.cwd(), operation, target, paths, cl
|
|
|
40
40
|
return manifest;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// Where the newest snapshot kept a path, relative to the project — what to tell someone whose own work
|
|
44
|
+
// was just replaced, so recovering it is a copy they can find rather than a restore they never heard of.
|
|
45
|
+
// A transaction snapshot is the fallback: it is the install's rollback, not the user's copy.
|
|
46
|
+
export function whereKept({ cwd = process.cwd(), relPath }) {
|
|
47
|
+
const backups = listBackups({ cwd });
|
|
48
|
+
const ordered = [...backups.filter((b) => !/transaction/.test(b.operation)), ...backups.filter((b) => /transaction/.test(b.operation))];
|
|
49
|
+
for (const b of ordered) {
|
|
50
|
+
const kept = join(backupsDir(cwd), b.id, 'files', relPath);
|
|
51
|
+
if (existsSync(kept)) return relative(cwd, kept).split(sep).join('/');
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
export function listBackups({ cwd = process.cwd() } = {}) {
|
|
44
57
|
const dir = backupsDir(cwd);
|
|
45
58
|
if (!existsSync(dir)) return [];
|
|
@@ -9,3 +9,17 @@ export const DEFAULT_SKILL_FLOOR = Object.freeze(['orient', 'suggest', 'bro']);
|
|
|
9
9
|
export function withDefaultSkillFloor(skillIds = []) {
|
|
10
10
|
return [...new Set([...DEFAULT_SKILL_FLOOR, ...skillIds])];
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
// Las skills del flujo que nombra el gate de código (targets/hook-once.mjs), más `retro`.
|
|
14
|
+
// Un proyecto con el gate conectado tiene que tenerlas todas: si el gate cambia y nombra una
|
|
15
|
+
// skill nueva (`implement-spec`, `pr`), el update la trae en vez de dejar el gate apuntando a
|
|
16
|
+
// algo que no está. Un proyecto sin gate (`codeHooks: false`) no recibe ninguna.
|
|
17
|
+
export const CODE_FLOW_FLOOR = Object.freeze([
|
|
18
|
+
'grill-with-docs', 'wayfinder', 'to-spec', 'write-adr', 'to-tickets',
|
|
19
|
+
'implement-spec', 'implement', 'code-review', 'pr', 'retro',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export function withCodeFlowFloor(skillIds = [], policy) {
|
|
23
|
+
if (policy?.codeHooks === false) return skillIds;
|
|
24
|
+
return [...new Set([...skillIds, ...CODE_FLOW_FLOOR])];
|
|
25
|
+
}
|
package/scripts/lib/domains.js
CHANGED
|
@@ -27,7 +27,7 @@ export const DOMAINS = [
|
|
|
27
27
|
{ title: 'Motion & interface craft', ids: ['motion-craft', 'ui-engineering', 'variant-explorer'] },
|
|
28
28
|
{ title: 'Design & content craft', ids: ['design-loop', 'design', 'design-dna', 'presentations', 'course-storytelling', 'course-builder', 'technical-writing', 'translation-l10n'] },
|
|
29
29
|
{ title: 'Knowledge & meta', ids: ['knowledge-ops', 'codebase-onboarding', 'research-ops', 'continuous-learning', 'skill-scout', 'context-budget', 'roast-me', 'show-me', 'eli5', 'fable-operator'] },
|
|
30
|
-
{ title: 'Personal catalog — workflow (Spanish)', ids: ['grill-me', 'grill-with-docs', 'grilling', 'wayfinder', 'to-spec', 'to-tickets', 'to-questionnaire', 'implement', 'revision-de-cambios', 'code-review', 'research', 'prototype', 'domain-modeling', 'handoff', 'write-adr', 'wait-what', 'writing-for-agents', 'claude-project-setup', 'muscle-memory', 'teach'] },
|
|
30
|
+
{ title: 'Personal catalog — workflow (Spanish)', ids: ['grill-me', 'grill-with-docs', 'grilling', 'wayfinder', 'to-spec', 'to-tickets', 'to-questionnaire', 'implement', 'implement-spec', 'pr', 'retro', 'revision-de-cambios', 'code-review', 'research', 'prototype', 'domain-modeling', 'handoff', 'write-adr', 'wait-what', 'writing-for-agents', 'claude-project-setup', 'muscle-memory', 'teach'] },
|
|
31
31
|
{ title: 'Personal catalog — interface design (Spanish)', ids: ['flujo-interfaz', 'animar', 'diseno-apple', 'diseno-landing', 'ingeniero-diseno-web', 'leyes-de-percepcion', 'leyes-de-retencion', 'mejor-accesibilidad', 'mejor-colores', 'mejor-layout', 'mejor-redaccion', 'mejor-tipografia', 'mejor-ui', 'sitios-calidad-premio', 'tastemaker', 'video-a-superprompt', 'vocabulario-animacion', 'revision-interfaz'] },
|
|
32
32
|
];
|
|
33
33
|
|
|
@@ -83,18 +83,69 @@ function recoveryCommand(plan, planId) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export function renderOnboardingDocuments(plan, planId) {
|
|
86
|
-
|
|
86
|
+
// `accompaniment_level`, never `accompaniment` (#276). It sits beside `technical_level` in the same
|
|
87
|
+
// block, and it is the name the nine skill files that read the dial — orient first among them —
|
|
88
|
+
// have always used. This was the only writer of the short form, which left the dial chosen at
|
|
89
|
+
// onboarding invisible to the compass.
|
|
90
|
+
const profile = `---\ntechnical_level: ${plan.record.technicalLevel}\naccompaniment_level: ${plan.record.accompaniment}\nproject_kind: ${plan.record.projectKind}\n---\n\n# User profile\n\nGoal: ${plan.record.goal}\n`;
|
|
87
91
|
const rows = plan.decisions.map((d) => `| ${d.kind} | ${d.id} | ${d.state} | ${d.reason} | ${d.reevaluateWhen.join('; ') || '—'} |`).join('\n');
|
|
88
92
|
const installation = `# Accepted harness plan\n\nPlan id: \`${planId}\`\n\n| Kind | Component | Decision | Reason | Reevaluate when |\n| --- | --- | --- | --- | --- |\n${rows}\n`;
|
|
89
93
|
const decisions = `# Harness decisions\n\n- Accepted plan \`${planId}\`.\n- Project kind: ${plan.record.projectKind}.\n- SDD: ${plan.decisions.find((d) => d.id === 'sdd')?.state || 'selected through profile'}.\n`;
|
|
90
94
|
return { profile, installation, decisions };
|
|
91
95
|
}
|
|
92
96
|
|
|
93
|
-
|
|
97
|
+
const PROFILE_DIALS = [['technical_level', 'technicalLevel'], ['accompaniment_level', 'accompaniment']];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The profile is the USER's file; accepting a plan seeds it, it does not own it (#278).
|
|
101
|
+
*
|
|
102
|
+
* `init` records there what it learns — the domain, the tools, what must not be touched — and `orient`
|
|
103
|
+
* rewrites the dial when someone asks for more or less explanation. Regenerating the file on every
|
|
104
|
+
* accept erased both, and re-accepting is routine: after an update, after a reassess.
|
|
105
|
+
*
|
|
106
|
+
* So: everything the plan does not own is kept byte for byte. `project_kind` and the goal are facts of
|
|
107
|
+
* the plan and are updated. The two dials are the subtle part — the plan carries them, but after first
|
|
108
|
+
* contact the user adjusts them — so an existing value stands when the plan did not CHANGE it relative
|
|
109
|
+
* to the previous receipt, and an explicit new value (a receipt that said L3, a plan that says L2) is
|
|
110
|
+
* applied. A profile written before the rename carries `accompaniment:`; it is migrated, never
|
|
111
|
+
* duplicated.
|
|
112
|
+
*/
|
|
113
|
+
export function mergeProfile(existing, plan, previousRecord) {
|
|
114
|
+
const fresh = renderOnboardingDocuments(plan, '').profile;
|
|
115
|
+
if (!existing || !existing.trim()) return fresh;
|
|
116
|
+
const text = existing.replace(/\r\n/g, '\n');
|
|
117
|
+
const block = /^---\n([\s\S]*?)\n---\n?/.exec(text);
|
|
118
|
+
let front = block ? block[1].split('\n') : [];
|
|
119
|
+
let body = block ? text.slice(block[0].length) : text;
|
|
120
|
+
const has = (key) => front.some((line) => line.startsWith(`${key}:`));
|
|
121
|
+
front = has('accompaniment_level')
|
|
122
|
+
? front.filter((line) => !/^accompaniment:\s/.test(line))
|
|
123
|
+
: front.map((line) => line.replace(/^accompaniment:(\s)/, 'accompaniment_level:$1'));
|
|
124
|
+
const get = (key) => front.find((line) => line.startsWith(`${key}:`))?.slice(key.length + 1).trim() || null;
|
|
125
|
+
const set = (key, value) => {
|
|
126
|
+
const at = front.findIndex((line) => line.startsWith(`${key}:`));
|
|
127
|
+
if (at >= 0) front[at] = `${key}: ${value}`;
|
|
128
|
+
else front.push(`${key}: ${value}`);
|
|
129
|
+
};
|
|
130
|
+
for (const [key, field] of PROFILE_DIALS) {
|
|
131
|
+
const planned = plan.record[field];
|
|
132
|
+
const keptByUser = get(key) && previousRecord && previousRecord[field] === planned;
|
|
133
|
+
if (!keptByUser) set(key, planned);
|
|
134
|
+
}
|
|
135
|
+
set('project_kind', plan.record.projectKind);
|
|
136
|
+
const goal = `Goal: ${plan.record.goal}`;
|
|
137
|
+
if (/^Goal: .*$/m.test(body)) body = body.replace(/^Goal: .*$/m, () => goal);
|
|
138
|
+
else if (/^# User profile[ \t]*$/m.test(body)) body = body.replace(/^# User profile[ \t]*$/m, () => `# User profile\n\n${goal}`);
|
|
139
|
+
else body = `${body.replace(/\s*$/, '')}\n\n${goal}\n`;
|
|
140
|
+
return `---\n${front.join('\n')}\n---\n${body.startsWith('\n') ? '' : '\n'}${body}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function writeOnboardingDocuments(cwd, plan, planId, previousRecord = null) {
|
|
94
144
|
const dir = join(cwd, 'docs', 'wiki', 'harness');
|
|
95
145
|
mkdirSync(dir, { recursive: true });
|
|
96
146
|
const docs = renderOnboardingDocuments(plan, planId);
|
|
97
|
-
|
|
147
|
+
const profilePath = join(dir, 'user-profile.md');
|
|
148
|
+
writeFileSync(profilePath, mergeProfile(existsSync(profilePath) ? readFileSync(profilePath, 'utf8') : '', plan, previousRecord));
|
|
98
149
|
writeFileSync(join(dir, 'installation-plan.md'), docs.installation);
|
|
99
150
|
const decisionsPath = join(dir, 'decisions.md');
|
|
100
151
|
if (!existsSync(decisionsPath)) writeFileSync(decisionsPath, docs.decisions);
|
|
@@ -134,12 +185,15 @@ export function verifyOnboarding(cwd, plan, planId) {
|
|
|
134
185
|
}
|
|
135
186
|
const docsDir = join(cwd, 'docs', 'wiki', 'harness');
|
|
136
187
|
const profile = existsSync(join(docsDir, 'user-profile.md')) ? readFileSync(join(docsDir, 'user-profile.md'), 'utf8') : '';
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
`
|
|
142
|
-
|
|
188
|
+
// The plan's facts must be exactly there. The dials must be there under their one name with a valid
|
|
189
|
+
// value — not necessarily the plan's, because a dial the user adjusted and the plan did not change
|
|
190
|
+
// is theirs (see mergeProfile).
|
|
191
|
+
for (const line of [`project_kind: ${plan.record.projectKind}`, `Goal: ${plan.record.goal}`]) {
|
|
192
|
+
if (!profile.split('\n').includes(line)) differences.push(`profile content differs: ${line.split(':')[0]}`);
|
|
193
|
+
}
|
|
194
|
+
if (!/^technical_level: (non-technical|mixed|technical)$/m.test(profile)) differences.push('profile content differs: technical_level');
|
|
195
|
+
if (!/^accompaniment_level: L[0-3]$/m.test(profile)) differences.push('profile content differs: accompaniment_level');
|
|
196
|
+
if (/^accompaniment: /m.test(profile)) differences.push('profile carries the retired accompaniment key');
|
|
143
197
|
const installation = existsSync(join(docsDir, 'installation-plan.md')) ? readFileSync(join(docsDir, 'installation-plan.md'), 'utf8') : '';
|
|
144
198
|
if (!installation.includes(`Plan id: \`${planId}\``)) differences.push('installation plan identity differs');
|
|
145
199
|
for (const decision of plan.decisions || []) {
|
|
@@ -148,7 +202,8 @@ export function verifyOnboarding(cwd, plan, planId) {
|
|
|
148
202
|
}
|
|
149
203
|
const decisions = existsSync(join(docsDir, 'decisions.md')) ? readFileSync(join(docsDir, 'decisions.md'), 'utf8') : '';
|
|
150
204
|
if (!decisions.includes(`Accepted plan \`${planId}\``)) differences.push('decision ledger omits accepted plan');
|
|
151
|
-
|
|
205
|
+
// Every planned assistant is declared; an extra one added outside the plan may be too (#278).
|
|
206
|
+
if (!plan.policy.targets.every((t) => (manifest?.targets || []).includes(t))) differences.push('manifest targets differ from accepted policy');
|
|
152
207
|
if (!sameSet(manifest?.skills || [], plan.policy.skills)) differences.push('manifest skills differ from accepted policy');
|
|
153
208
|
if (!sameSet(manifest?.agents || [], plan.policy.agents || [])) differences.push('manifest agents differ from accepted policy');
|
|
154
209
|
return differences;
|
|
@@ -200,7 +255,7 @@ export async function applyAcceptedOnboarding({ cwd = process.cwd(), plan, planI
|
|
|
200
255
|
});
|
|
201
256
|
}
|
|
202
257
|
pruneSharedBases({ cwd, skillIds: plan.policy.skills });
|
|
203
|
-
writeOnboardingDocuments(cwd, plan, planId);
|
|
258
|
+
writeOnboardingDocuments(cwd, plan, planId, previousPlan?.record || null);
|
|
204
259
|
receipt.provenance.paths = Object.fromEntries((plan.governedPaths || []).map((path) => [
|
|
205
260
|
path,
|
|
206
261
|
existed[path] ? (previousReceipt ? 'conserved' : 'preexisting') : 'installed',
|
|
@@ -209,7 +264,13 @@ export async function applyAcceptedOnboarding({ cwd = process.cwd(), plan, planI
|
|
|
209
264
|
const manifest = readManifest(cwd);
|
|
210
265
|
writeManifest(cwd, {
|
|
211
266
|
...manifest,
|
|
212
|
-
|
|
267
|
+
// Union, with the distinction the removal loop above already makes (#278). An assistant that was
|
|
268
|
+
// in the previous PLAN and is not in this one was removed on purpose. One added OUTSIDE the plan
|
|
269
|
+
// — `sync --target codex` — is still installed, and dropping it from the declaration meant a
|
|
270
|
+
// clone would never build it again.
|
|
271
|
+
targets: [...plan.policy.targets, ...(previousManifest?.targets || []).filter((t) => !plan.policy.targets.includes(t)
|
|
272
|
+
&& !(previousPlan?.policy?.targets || []).includes(t)
|
|
273
|
+
&& existsSync(targetPaths(t, undefined, cwd).stateFile))],
|
|
213
274
|
skills: [...plan.policy.skills],
|
|
214
275
|
agents: [...(plan.policy.agents || [])],
|
|
215
276
|
onboarding: receipt,
|
|
@@ -199,6 +199,13 @@ export function buildOnboardingPlan(record, evidence) {
|
|
|
199
199
|
const hooks = needsWorkflow;
|
|
200
200
|
const agents = baseAgents ? resolveAgentNames(skills, []).sort() : [];
|
|
201
201
|
const gitmojiGuard = hooks && normalized.targets.includes('claude');
|
|
202
|
+
// The danger guard is NOT code machinery, and tying it to `hooks` was the mistake behind #273. The
|
|
203
|
+
// three guards above need a code project with git — a branch to protect, a commit to lint. This one
|
|
204
|
+
// stops recursive deletes, history-rewriting pushes and friends, which are exactly as irreversible
|
|
205
|
+
// in an invoicing workspace, and `init` promises it by technical level: non-technical and mixed get
|
|
206
|
+
// it, whatever the project is. The guard also decides at runtime from the profile, so the plan only
|
|
207
|
+
// chooses whether it is present, never whether it bites.
|
|
208
|
+
const dangerGuard = normalized.technicalLevel !== 'technical';
|
|
202
209
|
const decisions = skills.map((id) => detectedSkills.includes(id)
|
|
203
210
|
? selected(id, 'skill', `Detected ${id} evidence inside the selected project root.`, 'workspace-evidence')
|
|
204
211
|
: selected(id, 'skill', needsWorkflow
|
|
@@ -248,6 +255,7 @@ export function buildOnboardingPlan(record, evidence) {
|
|
|
248
255
|
alwaysOn: true,
|
|
249
256
|
codeHooks: needsWorkflow,
|
|
250
257
|
gitmojiGuard,
|
|
258
|
+
dangerGuard,
|
|
251
259
|
memory: true,
|
|
252
260
|
context7: false,
|
|
253
261
|
plugins: needsWorkflow ? ['superpowers@claude-plugins-official'] : [],
|
package/scripts/lib/repair.js
CHANGED
|
@@ -131,22 +131,26 @@ export async function repair({ cwd = process.cwd(), target, home = homedir(), dr
|
|
|
131
131
|
|
|
132
132
|
const paths = targetPaths(target, home, cwd);
|
|
133
133
|
const ids = [...new Set([...Object.keys(readState(paths.stateFile).skills || {}), ...(readManifest(cwd)?.skills || [])])];
|
|
134
|
+
// The accepted plan governs a repair exactly as it governs a sync. Without it, `wireHook` saw `{}`,
|
|
135
|
+
// read `codeHooks !== false` as true and wired every guard the plan had declined — and the next
|
|
136
|
+
// `sync`, which does pass the policy, took them away again (#274). Restoring is not deciding.
|
|
137
|
+
const policy = readManifest(cwd)?.onboarding?.plan?.policy;
|
|
134
138
|
const backup = createBackup({
|
|
135
139
|
cwd, operation: 'repair', target,
|
|
136
|
-
paths: managedPathsForInstall({ skillIds: ids, target, home, cwd }),
|
|
140
|
+
paths: managedPathsForInstall({ skillIds: ids, target, home, cwd, policy }),
|
|
137
141
|
});
|
|
138
142
|
|
|
139
143
|
for (const f of todo) {
|
|
140
144
|
if (f.id === 'nested-layout') rmSync(join(paths.root, 'rsc'), { recursive: true, force: true });
|
|
141
|
-
if (f.id === 'duplicate-hooks') { unwireHook(target, paths); await applyInstall({ skillIds: ids, target, home, cwd, operation: 'repair' }); }
|
|
142
|
-
if (f.id === 'dangling-links') await applyInstall({ skillIds: ids, target, home, cwd, operation: 'repair' });
|
|
145
|
+
if (f.id === 'duplicate-hooks') { unwireHook(target, paths); await applyInstall({ skillIds: ids, target, home, cwd, operation: 'repair', policy }); }
|
|
146
|
+
if (f.id === 'dangling-links') await applyInstall({ skillIds: ids, target, home, cwd, operation: 'repair', policy });
|
|
143
147
|
if (f.id === 'no-manifest') recordInManifest({ cwd, target, skillIds: ids });
|
|
144
148
|
if (f.id === 'wrong-target') {
|
|
145
149
|
// Install where the project actually points, THEN take the old wiring down — in that
|
|
146
150
|
// order, so a failure halfway leaves a working harness rather than none. Taking the
|
|
147
151
|
// block out of the old file gives it back byte-identical: in #249 that file was the
|
|
148
152
|
// project's hand-written constitution, and it had already been written into once.
|
|
149
|
-
await applyInstall({ skillIds: ids, target: f.to, home, cwd, operation: 'repair' });
|
|
153
|
+
await applyInstall({ skillIds: ids, target: f.to, home, cwd, operation: 'repair', policy });
|
|
150
154
|
unwireHook(target, paths);
|
|
151
155
|
removeManagedSkills(paths);
|
|
152
156
|
recordInManifest({ cwd, target: f.to, skillIds: ids, dropTarget: target });
|