@ericrisco/rsc 2.0.20 → 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.
Files changed (92) hide show
  1. package/README.md +40 -17
  2. package/manifest.json +18 -85
  3. package/package.json +5 -1
  4. package/scripts/consult.js +8 -8
  5. package/scripts/install-apply.js +40 -11
  6. package/scripts/lib/default-skill-floor.js +5 -4
  7. package/scripts/lib/divergence.js +7 -3
  8. package/scripts/lib/domains.js +2 -2
  9. package/scripts/lib/onboarding-apply.js +43 -26
  10. package/scripts/lib/onboarding.js +5 -5
  11. package/scripts/lib/recommend.js +1 -1
  12. package/scripts/lib/repair.js +4 -2
  13. package/scripts/lib/retired-skills.js +20 -0
  14. package/scripts/rsc.js +25 -19
  15. package/scripts/skill-behavior-rubric.md +1 -1
  16. package/scripts/skill-migration-brief.md +3 -3
  17. package/skills/analyze/SKILL.md +3 -8
  18. package/skills/analyze/evals/cases.yaml +1 -1
  19. package/skills/author-skill/SKILL.md +3 -3
  20. package/skills/author-skill/evals/cases.yaml +1 -1
  21. package/skills/author-skill/references/rsc-conventions.md +1 -1
  22. package/skills/clarify/SKILL.md +7 -14
  23. package/skills/clarify/evals/cases.yaml +2 -2
  24. package/skills/code-review/SKILL.md +1 -1
  25. package/skills/constitution/SKILL.md +10 -13
  26. package/skills/constitution/evals/cases.yaml +3 -3
  27. package/skills/continuous-learning/SKILL.md +1 -1
  28. package/skills/continuous-learning/evals/cases.yaml +1 -1
  29. package/skills/debug/SKILL.md +8 -12
  30. package/skills/debug/evals/cases.yaml +1 -1
  31. package/skills/decision-challenge/SKILL.md +1 -1
  32. package/skills/deprecation/SKILL.md +1 -1
  33. package/skills/ftd/SKILL.md +1 -3
  34. package/skills/ftd/evals/cases.yaml +1 -1
  35. package/skills/harness/SKILL.md +10 -9
  36. package/skills/harness/references/claude-md-template.md +1 -1
  37. package/skills/harness/references/wiki-protocol.md +5 -6
  38. package/skills/idea-refinement/SKILL.md +1 -1
  39. package/skills/implement/SKILL.md +14 -18
  40. package/skills/implement/evals/cases.yaml +1 -1
  41. package/skills/init/SKILL.md +26 -44
  42. package/skills/init/evals/README.md +3 -3
  43. package/skills/init/evals/cases.yaml +7 -7
  44. package/skills/init/references/accompaniment-and-profile.md +22 -40
  45. package/skills/init/references/discovery.md +2 -2
  46. package/skills/init/references/recommend-skills.md +5 -5
  47. package/skills/init/scripts/verify.sh +3 -7
  48. package/skills/knowledge-ops/SKILL.md +1 -1
  49. package/skills/orient/SKILL.md +52 -41
  50. package/skills/orient/evals/README.md +11 -9
  51. package/skills/orient/evals/cases.yaml +39 -29
  52. package/skills/orient/references/explain-ladder.md +93 -0
  53. package/skills/orient/references/orientation-contract.md +70 -18
  54. package/skills/parallel/SKILL.md +1 -1
  55. package/skills/parallel/evals/cases.yaml +2 -2
  56. package/skills/plan/SKILL.md +9 -13
  57. package/skills/review/SKILL.md +5 -8
  58. package/skills/roast-me/SKILL.md +1 -1
  59. package/skills/sdd/SKILL.md +10 -14
  60. package/skills/sdd/evals/README.md +2 -2
  61. package/skills/sdd/evals/cases.yaml +4 -4
  62. package/skills/sdd/references/model-routing.md +5 -12
  63. package/skills/sdd-init/SKILL.md +3 -3
  64. package/skills/ship/SKILL.md +6 -11
  65. package/skills/simplify-code/SKILL.md +1 -1
  66. package/skills/skill-scout/SKILL.md +1 -1
  67. package/skills/source-grounded-development/SKILL.md +1 -1
  68. package/skills/specify/SKILL.md +13 -14
  69. package/skills/specify/evals/cases.yaml +1 -1
  70. package/skills/specify/references/eliciting-requirements.md +5 -5
  71. package/skills/suggest/SKILL.md +3 -9
  72. package/skills/suggest/evals/cases.yaml +1 -1
  73. package/skills/tasks/SKILL.md +6 -7
  74. package/skills/tasks/evals/cases.yaml +1 -1
  75. package/skills/unslop/SKILL.md +39 -10
  76. package/skills/unslop/evals/README.md +7 -7
  77. package/skills/unslop/evals/cases.yaml +10 -9
  78. package/skills/verify/SKILL.md +5 -8
  79. package/skills/verify/evals/cases.yaml +1 -1
  80. package/skills/worktrees/SKILL.md +14 -18
  81. package/skills/worktrees/evals/cases.yaml +1 -1
  82. package/targets/clone-bootstrap.mjs +4 -1
  83. package/targets/session-start.mjs +1 -1
  84. package/skills/bro/SKILL.md +0 -76
  85. package/skills/bro/evals/README.md +0 -22
  86. package/skills/bro/evals/cases.yaml +0 -62
  87. package/skills/eli5/SKILL.md +0 -63
  88. package/skills/eli5/evals/README.md +0 -22
  89. package/skills/eli5/evals/cases.yaml +0 -54
  90. package/skills/show-me/SKILL.md +0 -140
  91. package/skills/show-me/evals/README.md +0 -23
  92. package/skills/show-me/evals/cases.yaml +0 -61
@@ -9,9 +9,29 @@ import { readState } from './state.js';
9
9
  import { readManifest, writeManifest } from './manifest-file.js';
10
10
  import { encodeGoal, identifyPlan } from './onboarding.js';
11
11
  import { createBackup, restoreBackup } from './backups.js';
12
+ import { RETIRED_SKILLS, replaceRetired } from './retired-skills.js';
12
13
 
13
14
  const sameSet = (a = [], b = []) => JSON.stringify([...a].sort()) === JSON.stringify([...b].sort());
14
15
 
16
+ /**
17
+ * A receipt accepted before eli5, show-me and bro were retired still names them, and it cannot be
18
+ * rewritten — it is hash-checked against what the user accepted. So it is READ through the
19
+ * retirement: its skills as their successors, and the paths it governed for a retired skill (its
20
+ * link in each assistant and its base in `.rsc/skills/`) as no longer owed. Otherwise every upgraded
21
+ * harness would read as drift for having done exactly what the upgrade is meant to do.
22
+ */
23
+ const acceptedSkills = (plan) => replaceRetired(plan.policy.skills);
24
+ function retiredGovernedPaths(cwd, plan) {
25
+ const out = new Set();
26
+ for (const id of Object.keys(RETIRED_SKILLS)) {
27
+ out.add(`.rsc/skills/${id}`);
28
+ for (const target of plan.policy?.targets || []) {
29
+ out.add(relative(cwd, targetPaths(target, undefined, cwd).skillDir(id)).split(sep).join('/'));
30
+ }
31
+ }
32
+ return out;
33
+ }
34
+
15
35
  /**
16
36
  * What THIS target should be holding, which is not the same question as what the project decided.
17
37
  *
@@ -73,7 +93,6 @@ function recoveryCommand(plan, planId) {
73
93
  return [
74
94
  'npx @ericrisco/rsc@latest onboard',
75
95
  `--technical-level ${record.technicalLevel}`,
76
- `--accompaniment ${record.accompaniment}`,
77
96
  `--project-kind ${record.projectKind}`,
78
97
  `--goal-base64 ${encodeGoal(record.goal)}`,
79
98
  ...(record.softwareScope ? [`--software-scope ${record.softwareScope}`] : []),
@@ -83,18 +102,19 @@ function recoveryCommand(plan, planId) {
83
102
  }
84
103
 
85
104
  export function renderOnboardingDocuments(plan, planId) {
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`;
105
+ // One dial: `technical_level`. `technical` gets the technical register; `non-technical` and `mixed`
106
+ // get the one with analogies. The accompaniment dial (`accompaniment_level`, and the older
107
+ // `accompaniment` before #276) is retired and never written.
108
+ const profile = `---\ntechnical_level: ${plan.record.technicalLevel}\nproject_kind: ${plan.record.projectKind}\n---\n\n# User profile\n\nGoal: ${plan.record.goal}\n`;
91
109
  const rows = plan.decisions.map((d) => `| ${d.kind} | ${d.id} | ${d.state} | ${d.reason} | ${d.reevaluateWhen.join('; ') || '—'} |`).join('\n');
92
110
  const installation = `# Accepted harness plan\n\nPlan id: \`${planId}\`\n\n| Kind | Component | Decision | Reason | Reevaluate when |\n| --- | --- | --- | --- | --- |\n${rows}\n`;
93
111
  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`;
94
112
  return { profile, installation, decisions };
95
113
  }
96
114
 
97
- const PROFILE_DIALS = [['technical_level', 'technicalLevel'], ['accompaniment_level', 'accompaniment']];
115
+ const PROFILE_DIALS = [['technical_level', 'technicalLevel']];
116
+ // The retired accompaniment dial, under both names it ever had. Removed on rewrite; tolerated on read.
117
+ const RETIRED_PROFILE_KEY = /^accompaniment(_level)?:/;
98
118
 
99
119
  /**
100
120
  * The profile is the USER's file; accepting a plan seeds it, it does not own it (#278).
@@ -104,11 +124,10 @@ const PROFILE_DIALS = [['technical_level', 'technicalLevel'], ['accompaniment_le
104
124
  * accept erased both, and re-accepting is routine: after an update, after a reassess.
105
125
  *
106
126
  * 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.
127
+ * the plan and are updated. The dial is the subtle part — the plan carries it, but after first contact
128
+ * the user adjusts it — so an existing value stands when the plan did not CHANGE it relative to the
129
+ * previous receipt, and an explicit new value is applied. The retired accompaniment dial is the one
130
+ * thing taken OUT: its lines are dropped, whichever of its two names they use.
112
131
  */
113
132
  export function mergeProfile(existing, plan, previousRecord) {
114
133
  const fresh = renderOnboardingDocuments(plan, '').profile;
@@ -117,10 +136,7 @@ export function mergeProfile(existing, plan, previousRecord) {
117
136
  const block = /^---\n([\s\S]*?)\n---\n?/.exec(text);
118
137
  let front = block ? block[1].split('\n') : [];
119
138
  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'));
139
+ front = front.filter((line) => !RETIRED_PROFILE_KEY.test(line));
124
140
  const get = (key) => front.find((line) => line.startsWith(`${key}:`))?.slice(key.length + 1).trim() || null;
125
141
  const set = (key, value) => {
126
142
  const at = front.findIndex((line) => line.startsWith(`${key}:`));
@@ -159,7 +175,7 @@ export function verifyOnboarding(cwd, plan, planId) {
159
175
  if (identifyPlan(plan) !== planId) differences.push('persisted plan identity differs from its canonical content');
160
176
  for (const target of plan.policy.targets) {
161
177
  const state = readState(targetPaths(target, undefined, cwd).stateFile);
162
- if (!sameSet(Object.keys(state.skills || {}), plan.policy.skills)) differences.push(`${target}: installed skills differ from accepted policy`);
178
+ if (!sameSet(Object.keys(state.skills || {}), acceptedSkills(plan))) differences.push(`${target}: installed skills differ from accepted policy`);
163
179
  if (!sameSet(state.agents || [], expectedAgentsFor(target, plan))) differences.push(`${target}: installed agents differ from accepted policy`);
164
180
  if (state.policy?.alwaysOn !== plan.policy.alwaysOn) differences.push(`${target}: always-on policy differs`);
165
181
  if (state.policy?.codeHooks !== plan.policy.codeHooks) differences.push(`${target}: code-hook policy differs`);
@@ -169,7 +185,9 @@ export function verifyOnboarding(cwd, plan, planId) {
169
185
  for (const name of ['user-profile.md', 'installation-plan.md', 'decisions.md']) {
170
186
  if (!existsSync(join(cwd, '02-DOCS', 'wiki', 'harness', name))) differences.push(`missing ${name}`);
171
187
  }
172
- for (const path of plan.governedPaths || []) {
188
+ const retiredPaths = retiredGovernedPaths(cwd, plan);
189
+ const owed = (path) => !retiredPaths.has(path.replace(/\/$/, ''));
190
+ for (const path of (plan.governedPaths || []).filter(owed)) {
173
191
  if (!existsSync(join(cwd, path.replace(/\/$/, '')))) differences.push(`missing governed path ${path}`);
174
192
  }
175
193
  const manifest = readManifest(cwd);
@@ -177,23 +195,22 @@ export function verifyOnboarding(cwd, plan, planId) {
177
195
  const expectedDigests = manifest?.onboarding?.artifactDigests;
178
196
  if (!expectedDigests) differences.push('manifest receipt has no governed artifact digests');
179
197
  else {
180
- const expectedPaths = (plan.governedPaths || []).filter((path) => path !== '.rsc.json' && path !== '.rsc/backups/');
181
- if (!sameSet(Object.keys(expectedDigests), expectedPaths)) differences.push('governed artifact digest inventory differs from accepted plan');
198
+ const expectedPaths = (plan.governedPaths || []).filter((path) => path !== '.rsc.json' && path !== '.rsc/backups/').filter(owed);
199
+ if (!sameSet(Object.keys(expectedDigests).filter(owed), expectedPaths)) differences.push('governed artifact digest inventory differs from accepted plan');
182
200
  for (const path of expectedPaths) {
183
201
  if (digestPath(join(cwd, path.replace(/\/$/, ''))) !== expectedDigests[path]) differences.push(`governed content differs at ${path}`);
184
202
  }
185
203
  }
186
204
  const docsDir = join(cwd, '02-DOCS', 'wiki', 'harness');
187
205
  const profile = existsSync(join(docsDir, 'user-profile.md')) ? readFileSync(join(docsDir, 'user-profile.md'), 'utf8') : '';
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).
206
+ // The plan's facts must be exactly there. The dial must be there with a valid value — not
207
+ // necessarily the plan's, because a dial the user adjusted and the plan did not change is theirs
208
+ // (see mergeProfile). A leftover accompaniment line is not checked at all: it is retired, and an
209
+ // old profile that still carries one is not drift.
191
210
  for (const line of [`project_kind: ${plan.record.projectKind}`, `Goal: ${plan.record.goal}`]) {
192
211
  if (!profile.split('\n').includes(line)) differences.push(`profile content differs: ${line.split(':')[0]}`);
193
212
  }
194
213
  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');
197
214
  const installation = existsSync(join(docsDir, 'installation-plan.md')) ? readFileSync(join(docsDir, 'installation-plan.md'), 'utf8') : '';
198
215
  if (!installation.includes(`Plan id: \`${planId}\``)) differences.push('installation plan identity differs');
199
216
  for (const decision of plan.decisions || []) {
@@ -204,7 +221,7 @@ export function verifyOnboarding(cwd, plan, planId) {
204
221
  if (!decisions.includes(`Accepted plan \`${planId}\``)) differences.push('decision ledger omits accepted plan');
205
222
  // Every planned assistant is declared; an extra one added outside the plan may be too (#278).
206
223
  if (!plan.policy.targets.every((t) => (manifest?.targets || []).includes(t))) differences.push('manifest targets differ from accepted policy');
207
- if (!sameSet(manifest?.skills || [], plan.policy.skills)) differences.push('manifest skills differ from accepted policy');
224
+ if (!sameSet(replaceRetired(manifest?.skills || []), acceptedSkills(plan))) differences.push('manifest skills differ from accepted policy');
208
225
  if (!sameSet(manifest?.agents || [], plan.policy.agents || [])) differences.push('manifest agents differ from accepted policy');
209
226
  return differences;
210
227
  }
@@ -5,12 +5,12 @@ import { TARGET_IDS } from '../../targets/index.js';
5
5
  import { resolveAgentNames } from '../../targets/agents.js';
6
6
  import { loadManifest, skillsForProfile } from './manifest.js';
7
7
  import { readManifest } from './manifest-file.js';
8
+ import { replaceRetired } from './retired-skills.js';
8
9
  import { managedPathsForInstall } from '../install-apply.js';
9
10
 
10
11
  export const ONBOARDING_SCHEMA_VERSION = 1;
11
12
  export const ONBOARDING_VALUES = Object.freeze({
12
13
  technicalLevel: ['non-technical', 'mixed', 'technical'],
13
- accompaniment: ['L0', 'L1', 'L2', 'L3'],
14
14
  projectKind: ['software', 'operations', 'research', 'content', 'mixed'],
15
15
  softwareScope: ['small', 'growing', 'complex'],
16
16
  });
@@ -33,7 +33,9 @@ export function normalizeOnboarding(raw = {}) {
33
33
  const schemaVersion = Number(raw.schemaVersion ?? ONBOARDING_SCHEMA_VERSION);
34
34
  if (schemaVersion !== ONBOARDING_SCHEMA_VERSION) throw new Error(`invalid schemaVersion: expected ${ONBOARDING_SCHEMA_VERSION}`);
35
35
  const technicalLevel = oneOf('technical-level', clean(raw.technicalLevel).toLowerCase(), ONBOARDING_VALUES.technicalLevel);
36
- const accompaniment = oneOf('accompaniment', clean(raw.accompaniment).toUpperCase(), ONBOARDING_VALUES.accompaniment);
36
+ // `accompaniment` is retired: `technical_level` alone sets the register (technical, or with
37
+ // analogies). Old scripts still send it, so it is ignored here — not validated, not recorded, and
38
+ // therefore not part of the plan id either.
37
39
  const projectKind = oneOf('project-kind', clean(raw.projectKind).toLowerCase(), ONBOARDING_VALUES.projectKind);
38
40
  const goal = clean(raw.goal).replace(/\s+/g, ' ');
39
41
  if (!goal) throw new Error('invalid goal: a concrete goal is required');
@@ -48,7 +50,6 @@ export function normalizeOnboarding(raw = {}) {
48
50
  return {
49
51
  schemaVersion,
50
52
  technicalLevel,
51
- accompaniment,
52
53
  projectKind,
53
54
  goal,
54
55
  ...(needsScope ? { softwareScope } : {}),
@@ -223,7 +224,7 @@ export function buildOnboardingPlan(record, evidence) {
223
224
  // `readManifest` returns null on a first install, which is the most common path of all; reaching
224
225
  // through that null would have broken every fresh install in order to protect updates.
225
226
  const inAnyProfile = new Set(catalog.skills.filter((s) => (s.profiles || []).length).map((s) => s.id));
226
- const declared = (readManifest(evidence.root ?? process.cwd())?.skills ?? [])
227
+ const declared = replaceRetired(readManifest(evidence.root ?? process.cwd())?.skills ?? [])
227
228
  .filter((id) => catalogIds.has(id) && !inAnyProfile.has(id));
228
229
  const skills = [...new Set([...skillsForProfile(catalog, profile), ...detectedSkills, ...declared])].sort();
229
230
  const baseAgents = practisesSdd;
@@ -380,7 +381,6 @@ export function recommendDeferredComponents(acceptedPlan, currentEvidence) {
380
381
  export function missingOnboardingFields(raw = {}) {
381
382
  const missing = [];
382
383
  if (!clean(raw.technicalLevel)) missing.push('technical-level');
383
- if (!clean(raw.accompaniment)) missing.push('accompaniment');
384
384
  if (!clean(raw.projectKind)) missing.push('project-kind');
385
385
  if (!clean(raw.goal)) missing.push('goal');
386
386
  const kind = clean(raw.projectKind).toLowerCase();
@@ -1,7 +1,7 @@
1
1
  import { skillById } from './manifest.js';
2
2
 
3
3
  const OUTCOMES = {
4
- bro: 'Writing that sounds natural and human',
4
+ unslop: 'Writing for others that sounds natural and human',
5
5
  suggest: 'An assistant that proposes what you need as you go',
6
6
  harness: 'A tidy workspace: connect and document your company',
7
7
  init: 'Guided project bootstrap',
@@ -5,6 +5,7 @@ import { targetPaths, installedTargets, detectTarget, unwireHook } from '../../t
5
5
  import { readState } from './state.js';
6
6
  import { readManifest } from './manifest-file.js';
7
7
  import { createBackup } from './backups.js';
8
+ import { replaceRetired } from './retired-skills.js';
8
9
  import { countHookEntries } from '../doctor.js';
9
10
  import { applyInstall, recordInManifest, managedPathsForInstall } from '../install-apply.js';
10
11
 
@@ -56,7 +57,7 @@ export function diagnose({ cwd = process.cwd(), target, home = homedir(), invoke
56
57
 
57
58
  // A clone: the declaration travelled, the materialised skills did not.
58
59
  const dangling = installed.filter((id) => !existsSync(paths.skillDir(id)))
59
- .concat((manifest?.skills || []).filter((id) => !existsSync(paths.skillDir(id))));
60
+ .concat(replaceRetired(manifest?.skills || []).filter((id) => !existsSync(paths.skillDir(id))));
60
61
  if (dangling.length) {
61
62
  out.push(finding('dangling-links', 'restore',
62
63
  `${[...new Set(dangling)].join(', ')} are declared but not on disk — what a fresh clone looks like.`,
@@ -130,7 +131,8 @@ export async function repair({ cwd = process.cwd(), target, home = homedir(), dr
130
131
  }
131
132
 
132
133
  const paths = targetPaths(target, home, cwd);
133
- const ids = [...new Set([...Object.keys(readState(paths.stateFile).skills || {}), ...(readManifest(cwd)?.skills || [])])];
134
+ // Through the retirement map: a retired id cannot be rebuilt from the catalog, its successor can.
135
+ const ids = replaceRetired([...Object.keys(readState(paths.stateFile).skills || {}), ...(readManifest(cwd)?.skills || [])]);
134
136
  // The accepted plan governs a repair exactly as it governs a sync. Without it, `wireHook` saw `{}`,
135
137
  // read `codeHooks !== false` as true and wired every guard the plan had declined — and the next
136
138
  // `sync`, which does pass the policy, took them away again (#274). Restoring is not deciding.
@@ -0,0 +1,20 @@
1
+ // Skills the catalog no longer ships, and the skill that took over their job.
2
+ //
3
+ // `eli5` and `show-me` folded into `orient`, which explains at the register `technical_level` asks
4
+ // for; `bro` folded into `unslop`, which rewrites text meant for other people. A harness built before
5
+ // that still names them — in `.rsc.json`, in per-target state, and inside the onboarding receipt,
6
+ // which is hash-checked against what the user accepted and therefore never rewritten. So every reader
7
+ // of a DECLARED skill list maps through here, instead of each one learning the history on its own.
8
+ //
9
+ // `targets/clone-bootstrap.mjs` is committed into user repos and cannot import this file; it carries
10
+ // an inline copy, and tests/retired-skills.test.js holds the two equal in behaviour.
11
+ export const RETIRED_SKILLS = Object.freeze({ eli5: 'orient', 'show-me': 'orient', bro: 'unslop' });
12
+
13
+ const retiredTo = (id) => (Object.hasOwn(RETIRED_SKILLS, id) ? RETIRED_SKILLS[id] : id);
14
+
15
+ /** Each retired id becomes its successor; order of first appearance kept, duplicates dropped. */
16
+ export function replaceRetired(ids = []) {
17
+ return [...new Set((ids || []).map(retiredTo))];
18
+ }
19
+
20
+ export const isRetired = (id) => Object.hasOwn(RETIRED_SKILLS, id);
package/scripts/rsc.js CHANGED
@@ -15,6 +15,7 @@ import { listBackups, restoreBackup, whereKept } from './lib/backups.js';
15
15
  import { runUpgrade } from './lib/upgrade.js';
16
16
  import { diagnose, repair } from './lib/repair.js';
17
17
  import { DEFAULT_SKILL_FLOOR, withDefaultSkillFloor } from './lib/default-skill-floor.js';
18
+ import { RETIRED_SKILLS, replaceRetired } from './lib/retired-skills.js';
18
19
  import { readManifest, writeManifest } from './lib/manifest-file.js';
19
20
  import { versionReport } from './lib/versions.js';
20
21
  import {
@@ -38,6 +39,8 @@ if (['--version', '-v', 'version'].includes(rawArgv[0])) {
38
39
  process.stdout.write(`${lines.join('\n')}\n`);
39
40
  process.exit(0);
40
41
  }
42
+ // `--accompaniment` stays listed although the dial is retired: old scripts and agents still pass it,
43
+ // and its value must keep being skipped rather than read as a command or a skill id.
41
44
  const GLOBAL_VALUE_FLAGS = new Set([
42
45
  '--target', '--technical-level', '--accompaniment', '--project-kind', '--goal', '--goal-base64', '--software-scope', '--accept-plan',
43
46
  ]);
@@ -78,7 +81,6 @@ function onboardingInput(targets) {
78
81
  return {
79
82
  schemaVersion: 1,
80
83
  technicalLevel: value('technical-level'),
81
- accompaniment: value('accompaniment'),
82
84
  projectKind: value('project-kind'),
83
85
  goal: value('goal') || (value('goal-base64') ? decodeGoal(value('goal-base64')) : undefined),
84
86
  softwareScope: value('software-scope'),
@@ -91,7 +93,7 @@ function onboardingRequired(raw, action = 'onboard') {
91
93
  code: 'RSC_ONBOARDING_REQUIRED',
92
94
  schemaVersion: 1,
93
95
  missing: missingOnboardingFields(raw),
94
- recovery: `Run npx @ericrisco/rsc@latest ${action} --technical-level <non-technical|mixed|technical> --accompaniment <L0|L1|L2|L3> --project-kind <software|operations|research|content|mixed> --goal "<what you want>" --target <assistant>`,
96
+ recovery: `Run npx @ericrisco/rsc@latest ${action} --technical-level <non-technical|mixed|technical> --project-kind <software|operations|research|content|mixed> --goal "<what you want>" --target <assistant>`,
95
97
  };
96
98
  console.error(`RSC_ONBOARDING_REQUIRED ${JSON.stringify(payload)}`);
97
99
  process.exitCode = 2;
@@ -152,7 +154,6 @@ function renderPlan(plan, planId) {
152
154
  const pieces = [
153
155
  'npx @ericrisco/rsc@latest onboard',
154
156
  `--technical-level ${plan.record.technicalLevel}`,
155
- `--accompaniment ${plan.record.accompaniment}`,
156
157
  `--project-kind ${plan.record.projectKind}`,
157
158
  `--goal-base64 ${encodeGoal(plan.record.goal)}`,
158
159
  ...(plan.record.softwareScope ? [`--software-scope ${plan.record.softwareScope}`] : []),
@@ -167,16 +168,11 @@ async function runOnboarding(targets) {
167
168
  if (isInteractive() && missingOnboardingFields(raw).length) {
168
169
  await banner(loadManifest().counts.skills);
169
170
  say('Before rsc writes anything, it will learn how to help and show the exact harness plan.');
170
- raw.technicalLevel ||= await select('How technical should the conversation be?', [
171
- { key: 'non-technical', label: 'Plain language — explain terms and avoid code jargon' },
172
- { key: 'mixed', label: 'Mixed — concise explanations with useful technical detail' },
171
+ // One question about the person, not two: the register. `--technical-level mixed` is still
172
+ // accepted from the command line and reads like non-technical — with analogies.
173
+ raw.technicalLevel ||= await select('How should I talk to you?', [
173
174
  { key: 'technical', label: 'Technical — assume I am comfortable with code and tooling' },
174
- ]);
175
- raw.accompaniment ||= await select('How much accompaniment do you want?', [
176
- { key: 'L0', label: 'L0 — results only' },
177
- { key: 'L1', label: 'L1 — brief reasons, questions only when needed' },
178
- { key: 'L2', label: 'L2 — explain each relevant decision' },
179
- { key: 'L3', label: 'L3 — guide me through everything' },
175
+ { key: 'non-technical', label: 'With analogies and plain words — explain terms, no jargon' },
180
176
  ]);
181
177
  raw.projectKind ||= await select('What are you building or running?', [
182
178
  { key: 'software', label: 'Software or a website' },
@@ -194,7 +190,7 @@ async function runOnboarding(targets) {
194
190
  ]);
195
191
  }
196
192
  if (!raw.targets?.length) raw.targets = await selectAgents();
197
- if (!raw.technicalLevel || !raw.accompaniment || !raw.projectKind || !raw.goal || !raw.targets?.length) {
193
+ if (!raw.technicalLevel || !raw.projectKind || !raw.goal || !raw.targets?.length) {
198
194
  say('Cancelled — nothing was touched.');
199
195
  return;
200
196
  }
@@ -289,7 +285,6 @@ function runReassessment() {
289
285
  say([
290
286
  'npx @ericrisco/rsc@latest onboard',
291
287
  `--technical-level ${record.technicalLevel}`,
292
- `--accompaniment ${record.accompaniment}`,
293
288
  `--project-kind ${record.projectKind}`,
294
289
  `--goal-base64 ${encodeGoal(record.goal)}`,
295
290
  ...(record.softwareScope ? [`--software-scope ${record.softwareScope}`] : []),
@@ -423,8 +418,9 @@ function printNextSteps(targets, ids) {
423
418
  say(' "sdd-init" then "I want <your idea>"');
424
419
  say(' → walks you specify → plan → tasks → implement → verify → ship.');
425
420
  }
426
- say(` ${n++}. From there, work in your own words. orient + suggest stay always-on; bro is ready on request:`);
427
- say(' they keep you oriented, propose missing skills, and rewrite in plain human language when asked.');
421
+ say(` ${n++}. From there, work in your own words. orient + suggest stay always-on; unslop is ready on request:`);
422
+ say(' orient talks in short stand-alone sentences (technical or with analogies) and draws a diagram if you do not follow;');
423
+ say(' suggest proposes missing skills; unslop rewrites text you send to other people.');
428
424
  say('\n Add something by hand anytime: npx @ericrisco/rsc add <skill>');
429
425
  say(' Browse the catalog / get picks: npx @ericrisco/rsc consult "whatever you need"');
430
426
  say('────────────────────────────────────────────────────────');
@@ -444,7 +440,7 @@ function printAgentHandoff() {
444
440
  say('AGENT, do this now:');
445
441
  say(' 1. Reload/restart this session so the new skills + hooks load');
446
442
  say(' (Claude Code: restart the session · Codex/AGENTS.md tools: next turn).');
447
- say(' 2. After reload you are EQUIPPED — orient + suggest are always-on; bro is installed.');
443
+ say(' 2. After reload you are EQUIPPED — orient + suggest are always-on: short stand-alone answers, a diagram when you do not follow; unslop is installed to rewrite text for others.');
448
444
  // Un recibo cuya identidad no cuadra con lo que se aceptó no es fuente de nada: el camino de
449
445
  // `onboard` está a salvo porque reconstruye el plan y `identifyPlan` corta, y este no lo hacía.
450
446
  // Y va envuelto porque antes esta función no leía nada y no podía tumbar un `install` ya hecho.
@@ -665,14 +661,20 @@ async function main() {
665
661
  return runReassessment();
666
662
  case 'add': {
667
663
  if (!hasDeclaredHarness()) return onboardingRequired(onboardingInput(targets));
668
- const requested = requestedIds();
664
+ // A retired id is not unknown: it has a successor, and old notes, scripts and agents still
665
+ // name it. Say so in one line and add the successor instead.
666
+ const asked = requestedIds();
667
+ for (const id of asked.filter((id) => Object.hasOwn(RETIRED_SKILLS, id))) {
668
+ say(`${id} was retired and replaced by ${RETIRED_SKILLS[id]} — adding ${RETIRED_SKILLS[id]}.`);
669
+ }
670
+ const requested = replaceRetired(asked);
669
671
  const selected = classifyRequested(requested);
670
672
  if (reportUnknown(selected.unknown)) return;
671
673
  const ids = withDefaultSkillFloor(selected.skills);
672
674
  if (!argv.includes('--force') && !(await guardCollisions(targets, ids))) return;
673
675
  const currentManifest = readManifest();
674
676
  const receipt = currentManifest?.onboarding;
675
- const maintainedIds = receipt ? [...new Set([...(currentManifest.skills || []), ...(receipt.plan.policy.skills || []), ...ids])].sort() : ids;
677
+ const maintainedIds = receipt ? replaceRetired([...(currentManifest.skills || []), ...(receipt.plan.policy.skills || []), ...ids]).sort() : ids;
676
678
  const { policy, onboarding } = governedBy(receipt, maintainedIds);
677
679
  for (const t of targets) await applyInstall({ skillIds: maintainedIds, agentIds: selected.agents, target: t, policy, onboarding });
678
680
  markMaintenanceDrift(`add ${requested.join(',')}`);
@@ -1143,6 +1145,10 @@ async function main() {
1143
1145
  // `uninstall --all` is an alias for a full purge.
1144
1146
  if (argv.includes('--all')) return void (await runPurge(dry, argv.includes('--with-docs')));
1145
1147
  const selected = classifyRequested(requestedIds());
1148
+ // A retired skill an old install still holds is not unknown: the state file proves it is ours.
1149
+ const held = new Set(listInstalled({ target }));
1150
+ selected.skills.push(...selected.unknown.filter((id) => Object.hasOwn(RETIRED_SKILLS, id) && held.has(id)));
1151
+ selected.unknown = selected.unknown.filter((id) => !selected.skills.includes(id));
1146
1152
  if (reportUnknown(selected.unknown)) return;
1147
1153
  const removed = await uninstall({ skillIds: selected.skills, agentIds: selected.agents, target, dryRun: dry });
1148
1154
  return void say((dry ? 'Would remove:\n' : 'Removed:\n') + (removed.join('\n') || '(nothing)'));
@@ -27,7 +27,7 @@ deterministic and auditable.
27
27
  User-facing, conversational skills should keep the user oriented. When a skill's capability scenario ends a turn the user acts on, the grader expects the output to:
28
28
 
29
29
  - **Situate** the user (where they are / what is built vs missing).
30
- - **Teach the why**, scaled to the dial (`technical_level` / `accompaniment_level`).
30
+ - **Teach the why**, in the register `technical_level` sets (technical, or with analogies) — the voice is `orient`'s.
31
31
  - **Propose a next step phrased as a question** — never end in seco.
32
32
 
33
33
  Encode this as `must_include` items in those skills' capability scenarios (see `skills/orient/evals/cases.yaml` for the canonical pattern). It scores through the existing coverage axis — no separate formula. Skills that own a purely mechanical task (lint, a single rename) are exempt and should not be penalized.
@@ -40,8 +40,8 @@ The goal is the smallest body that still routes correctly. 400 lines is a ceilin
40
40
  - Rule banks that restate the flow — "Iron rules (non-negotiable)", "Rationalizations — STOP".
41
41
  A rule worth keeping moves next to the step it governs, and says *why* it is absolute instead of
42
42
  shouting NON-NEGOTIABLE. A rule in an appendix is skimmed; a rule in context is followed.
43
- - Re-teaching of what another skill owns. The accompaniment dial belongs to `init`; the SDD method
44
- belongs to `sdd`. Read the profile and point; do not restate, or the two drift apart.
43
+ - Re-teaching of what another skill owns. The register (`technical_level` in the profile, set by `init`) and
44
+ the voice that applies it belong to `orient`; the SDD method belongs to `sdd`. Read the profile and point; do not restate, or the two drift apart.
45
45
  - "When to use" sections that restate the description, and trailing reference indexes whose links
46
46
  already appear inline at point of use.
47
47
 
@@ -58,7 +58,7 @@ The goal is the smallest body that still routes correctly. 400 lines is a ceilin
58
58
  in one skill and keeping it in the next is worse than either choice applied uniformly.
59
59
  - Decision tables where the flow genuinely branches.
60
60
  - Tables specific to *this* skill's phase (e.g. what to show at each checkpoint) — that is not the
61
- generic dial.
61
+ retired verbosity levels.
62
62
  - Worked bad/good pairs that teach judgment by demonstration.
63
63
  - Result-envelope blocks, chain position, and output contracts where they already exist. Do **not**
64
64
  add one that was never there: that is a content change, not a format change.
@@ -17,14 +17,9 @@ Sixth phase of the rsc-sdd chain (`constitution → specify → clarify → plan
17
17
 
18
18
  **Model tier: `heavy`** (adversarial cross-reading). Resolve and apply it per `../sdd/references/model-routing.md`; routing is off unless `models.enabled: true` in `02-DOCS/wiki/sdd/config.yaml`.
19
19
 
20
- **Accompaniment dial.** Read `02-DOCS/wiki/harness/user-profile.md` before reporting — default to **L2** and say the harness has not gauged the user yet if there is no profile. The dial flexes how the report reads, never what gets checked; the six analyses always run in full.
20
+ **Register.** Read `technical_level` in `02-DOCS/wiki/harness/user-profile.md` before reporting; with no profile, use analogies and say the harness has not set the register yet. The register changes how the report reads, never what gets checked; the six analyses always run in full.
21
21
 
22
- | Dial | The report renders as |
23
- | --- | --- |
24
- | L0 | Finding table only: severity, the two artifacts, the conflict in one line. No prose. |
25
- | L1 | + a one-line *why it matters* per CRITICAL/HIGH finding. |
26
- | L2 | + per finding, the recommended resolution phase and the trade-off of leaving it. |
27
- | L3 | + full walk-through: quote both sides, explain the consequence at implement time in plain language, lay out the options so a non-technical user can choose. |
22
+ The report is the same for every reader: a finding table (severity, the two artifacts, the conflict in one line), a one-line *why it matters* per CRITICAL/HIGH finding, and per finding the recommended resolution phase and the trade-off of leaving it. For a `non-technical` reader, explain the consequence at implement time with an everyday analogy so they can choose.
28
23
 
29
24
  ## Inputs — locate the four artifacts
30
25
 
@@ -89,7 +84,7 @@ Produce a single consistency report:
89
84
 
90
85
  Write the report to `02-DOCS/wiki/sdd/analysis/<slug>.md` (create the dir if absent) and index it in `02-DOCS/wiki/index.md` under the `sdd/` topic, so the next phase and the harness can find it. It is an OKF v0.1 wiki article: open it with YAML frontmatter carrying a non-empty `type:` (use `type: analysis`), a `timestamp` in ISO 8601, and standard markdown links — never wikilinks. The report is the artifact analyze owns — it is the *only* thing analyze writes. Per-run point-in-time; overwrite on re-run, the wiki keeps history.
91
86
 
92
- Render it at the dial's verbosity. Do not log a decision to `decisions.md` — analyze decides nothing; the phase that resolves the finding logs its own decision.
87
+ Render it in the orient voice, in the user's register. Do not log a decision to `decisions.md` — analyze decides nothing; the phase that resolves the finding logs its own decision.
93
88
 
94
89
  ## Anti-patterns
95
90
 
@@ -64,7 +64,7 @@ capability:
64
64
  - scenario: "I have constitution.md, a spec, a plan and a task list under 02-DOCS/wiki/sdd/ for a 'team invites' feature. The constitution says 'Postgres only, every endpoint rate-limited'. The spec requires email invites with a 5/min rate limit and invite expiry after 7 days. The plan adds a Redis-backed cache nobody asked for and is silent on rate limiting. The tasks cover sending invites and expiry but have no task for rate limiting and no done-check on the expiry task. Run the gate."
65
65
  must_include:
66
66
  - "Reports only and resolves nothing: explicitly does NOT edit the spec/plan/tasks and writes no code; hands each finding back to the user to resolve."
67
- - "Reads the harness accompaniment dial from 02-DOCS/wiki/harness/user-profile.md and adapts report verbosity (or notes no profile and defaults to L2)."
67
+ - "Reads technical_level from 02-DOCS/wiki/harness/user-profile.md and writes the report in that register (or notes no profile and uses analogies), with every finding in one line."
68
68
  - "Runs all six analyses (constitution compliance, coverage, scope drift, contradiction, duplication, ambiguity), not just coverage."
69
69
  - "Flags the missing rate-limit task as a coverage GAP and as a CONSTITUTION violation (constitution requires every endpoint rate-limited) — marks it CRITICAL/HIGH and routes it to the tasks/plan phase."
70
70
  - "Flags the Redis cache as scope DRIFT (no spec requirement behind it) and notes the resolution is the user's call: cut it or amend the spec."
@@ -14,7 +14,7 @@ Where the SDD chain (`specify` → `plan` → … → `ship`) builds *product*,
14
14
 
15
15
  **Not this skill — delegate:** a product feature specced or planned → `../specify/SKILL.md`, `../plan/SKILL.md`. An autonomous agent or tool-calling loop → `../building-agents/SKILL.md`. Generic project docs or a wiki article → the `../harness/SKILL.md` 02-DOCS engine. Bootstrapping a workspace or profiling the user → `../init/SKILL.md`.
16
16
 
17
- Read `02-DOCS/wiki/harness/user-profile.md` and work at the accompaniment dial it records; `../init/SKILL.md` owns that dial and sets it. With no profile, default to non-technical framing and ask for the technical level and the dial before going deep — skill authoring is itself a technical act, so many users want more narration here than they do elsewhere.
17
+ Read `02-DOCS/wiki/harness/user-profile.md` and speak in the register its `technical_level` records (technical terms, or plain words with analogies); `../init/SKILL.md` sets it, and `orient` owns the voice. With no profile, use analogies and ask once "technical or with analogies?" — skill authoring is itself a technical act, so a non-technical author needs each convention explained, not just named.
18
18
 
19
19
  ## What a skill is (the anatomy)
20
20
 
@@ -84,7 +84,7 @@ Every new skill MUST end with the orientation footer so the harness never leaves
84
84
 
85
85
  ## Orientación (siempre)
86
86
 
87
- Cierra cada turno con el **bloque-brújula** (📍 dónde estás · ✅ qué hiciste · 🧭 por qué · ➡️ siguiente, terminando en pregunta), calibrado al dial de `02-DOCS/wiki/harness/user-profile.md`. **Nunca termines en seco.** Protocolo completo: skill `orient` → `skills/orient/references/orientation-contract.md`. (Defiere a `suggest` el "¿instalo la skill que falta?".)
87
+ Habla con la voz de `orient`: frases cortas, una idea por frase, y cada respuesta se entiende sola. Registro técnico o con analogías según `technical_level` en `02-DOCS/wiki/harness/user-profile.md`. Cierra cada turno con el **bloque-brújula** (📍 dónde estás · ➡️ siguiente, terminando en pregunta; ✅ y 🧭 cuando hay algo hecho o decidido). **Nunca termines en seco.** Protocolo completo: skill `orient` → `skills/orient/references/orientation-contract.md`. (Defiere a `suggest` el "¿instalo la skill que falta?".)
88
88
  ````
89
89
 
90
90
  The full protocol lives once in the `orient` skill; the footer only references it.
@@ -143,7 +143,7 @@ A skill ships only when every box is checked or a miss is consciously justified.
143
143
  - [ ] **Every `references/` file linked** inline from the body; none orphaned.
144
144
  - [ ] **Every fence language-tagged**; no placeholder/TODO prose; examples concrete.
145
145
  - [ ] **Checklist/decision table only where a flow branches**; an **anti-patterns table** present, naming failure modes rather than restating rules.
146
- - [ ] **Accompaniment dial honored** — reads the profile, adapts verbosity.
146
+ - [ ] **Orient voice honored** — reads `technical_level` from the profile and picks the register; short sentences, every answer understandable on its own.
147
147
  - [ ] **Artifacts under `02-DOCS/wiki/`** and indexed in `02-DOCS/wiki/index.md` (the Knowledge map; root `CLAUDE.md` keeps only a short pointer), if the skill produces any.
148
148
  - [ ] **Concrete tooling delegated** to the stack skills rather than reinvented.
149
149
  - [ ] **evals present** — `cases.yaml` (≥5 `should_trigger` incl. non-obvious, ≥4 `should_not_trigger` each with a real-sibling `route_to`, ≥1 `capability` with a `must_include` rubric) + an honest `README.md`. `scripts/eval-lint.sh` passes — but it only checks presence and the counts (≥5/≥4/≥1) and that those keys are lists; the `route_to`-points-at-a-real-sibling, non-obvious phrasings, and `must_include` quality are yours to verify here, not the linter's.
@@ -55,7 +55,7 @@ should_not_trigger:
55
55
  capability:
56
56
  - scenario: "A user says: 'I keep telling the agent how to write our PR descriptions. Make that into a skill called pr-describe.' Show how author-skill produces it to the rsc bar."
57
57
  must_include:
58
- - "Reads 02-DOCS/wiki/harness/user-profile.md for the accompaniment dial (or asks the two gauging questions if absent) and adapts verbosity"
58
+ - "Reads technical_level from 02-DOCS/wiki/harness/user-profile.md (or asks once 'technical or with analogies?' if absent) and explains in that register, without any verbosity level"
59
59
  - "Confirms scope is one job and checks no existing sibling already owns PR-description authoring before creating a new skill"
60
60
  - "Drafts the description FIRST: third-person, 'Use when…' lead, only the capabilities that discriminate it from its nearest sibling, a 'NOT … (that is `sibling`)' boundary, valid single-line quoted YAML aiming <=350 chars (1024 hard limit), with origin: risco"
61
61
  - "Produces the smallest body that still routes (400 lines is a ceiling, not a target) with progressive disclosure — long/branch-specific detail pushed to references/ and pointed to inline, not an orphaned references/ folder"
@@ -43,7 +43,7 @@ profiles: [core, full] # optional: named-profile membership
43
43
  - `tags` drive the FTS recommender — pick the words a user would actually type.
44
44
  - `recommends` must reference **real skill ids**; `build-manifest.js --validate`
45
45
  fails on a dangling id.
46
- - `profiles` is optional. `minimal` = the floor (`orient`, `suggest`, `bro`, `harness`, `init`);
46
+ - `profiles` is optional. `minimal` = the floor (`orient`, `suggest`, `unslop`, `ftd`, `harness`, `init`);
47
47
  `core` = the SDD workflow; `full` = everything. Most stack skills set `[full]`
48
48
  or omit it (they are installed on demand, not by profile).
49
49
 
@@ -15,14 +15,7 @@ This is the fourth phase of the rsc SDD chain (`constitution` → `specify` →
15
15
 
16
16
  **Model tier: `balanced`** — this phase ranks and asks the few high-leverage questions, it does not design architecture. Resolve and apply it per `../sdd/references/model-routing.md`; routing is off unless `models.enabled: true` in `02-DOCS/wiki/sdd/config.yaml`.
17
17
 
18
- **Accompaniment dial.** Read the level from `02-DOCS/wiki/harness/user-profile.md` (the dial and the `02-DOCS/wiki/` convention are owned by `../harness/SKILL.md`). Clarify is question-heavy, so the dial matters here more than almost anywhere — it sets **how many questions you ask and how you frame them**. With no profile: default to non-technical framing, ask the two gauging questions (technical level + accompaniment) first, then proceed at the stated level.
19
-
20
- | Dial | Ask |
21
- | --- | --- |
22
- | **L0** "cavernícola" | ONLY the questions whose answer changes the architecture or scope. Propose safe defaults for everything else and list them tersely as "assumed unless you object". Minimal prose. |
23
- | **L1** "breve" | The high-leverage batch, one line of *why* per question. |
24
- | **L2** "explica decisiones" | The batch plus the trade-off behind each option, so the user chooses informed. |
25
- | **L3** "acompañamiento total" | Walk the taxonomy out loud, explain what each kind of gap costs if left unresolved, ask broadly (including the medium-leverage questions), and teach the *why* as you go. Ideal for non-technical users who benefit from seeing the hidden decisions. |
18
+ **Register.** Read `technical_level` from `02-DOCS/wiki/harness/user-profile.md` (the `02-DOCS/wiki/` convention is owned by `../harness/SKILL.md`). Clarify is question-heavy, so the voice matters here more than almost anywhere. Ask only the questions whose answer changes the build; propose safe defaults for everything else and list them as "assumed unless you object". Each question carries one line of *why* and the trade-off behind each option, in the `orient` voice. For a `non-technical` reader, frame each option with an everyday analogy. With no profile: use analogies and ask once "technical or with analogies?".
26
19
 
27
20
  ## Read first — the inputs
28
21
 
@@ -30,7 +23,7 @@ Clarify never works blind. Before asking a single question, load three things:
30
23
 
31
24
  1. **The spec.** Read the target spec under `02-DOCS/wiki/sdd/specs/<slug>.md` end to end. If the path wasn't given, find the most recently touched spec or ask which one. Its *Points to clarify* is a **typed** handoff, not a question list — read the types before you plan a single question (below).
32
25
  2. **The constitution.** Read `02-DOCS/wiki/sdd/constitution.md` if it exists. Its principles (stack canon, quality bars, conventions) resolve a surprising number of "ambiguities" without bothering the user — if the constitution already fixes the auth method or the data region, that's answered, not open.
33
- 3. **The harness profile.** `02-DOCS/wiki/harness/user-profile.md`, for the dial above.
26
+ 3. **The harness profile.** `02-DOCS/wiki/harness/user-profile.md`, for the register above.
34
27
 
35
28
  Citing what you read ("checked the constitution — auth is already fixed to OAuth, so that's not an open question") shows your work and prevents re-litigating settled decisions.
36
29
 
@@ -85,13 +78,13 @@ Run in order. The discipline is: find many candidate gaps, keep only the ones th
85
78
 
86
79
  2. **Resolve what you already can.** For each candidate, check the constitution and the spec's own later sections before asking the user. Many "gaps" are answered elsewhere. Mark each candidate **resolved-internally** (cite the source), **inferable** (a safe default you'll propose, not silently assume), or **must-ask** (only the user can decide).
87
80
 
88
- 3. **Rank by leverage.** Sort the must-ask list by impact: how much does the build change depending on the answer? A question whose two answers lead to two different architectures ranks above a cosmetic one. Cut low-leverage questions — clarify is not an interrogation, it's the *few* questions that matter. Cap the batch to the dial.
81
+ 3. **Rank by leverage.** Sort the must-ask list by impact: how much does the build change depending on the answer? A question whose two answers lead to two different architectures ranks above a cosmetic one. Cut low-leverage questions — clarify is not an interrogation, it's the *few* questions that matter.
89
82
 
90
- 4. **Ask — one focused batch, sized to the dial.** How you ask determines whether you get a usable answer:
83
+ 4. **Ask — one focused batch.** How you ask determines whether you get a usable answer:
91
84
  - **Make it a decision, not an essay prompt.** "Should deletes be soft (recoverable, hidden) or hard (gone immediately)? I'd recommend soft because the spec mentions an audit trail — confirm?" beats "How should deletion work?".
92
85
  - **Carry your own recommendation** when there's a defensible default, matched to the constitution. The user confirms or overrides — far less effort than authoring from scratch.
93
86
  - **Quote the spec.** Anchor each question to the exact line or section it came from, so the user sees *why* it's open.
94
- - **One batch, ranked, then stop.** Don't drip questions one at a time over many turns unless the dial is L3; don't dump thirty at once. Then wait: never ask and answer in the same breath, and never assume the user's intent on a must-ask item.
87
+ - **One batch, ranked, then stop.** Don't drip questions one at a time over many turns; don't dump thirty at once. Then wait: never ask and answer in the same breath, and never assume the user's intent on a must-ask item.
95
88
 
96
89
  5. **Bake the answers back into the spec.** This is the deliverable — an un-baked answer is a lost answer. For each resolved item, edit the spec in place:
97
90
  - Tighten the relevant section with the decided behavior.
@@ -135,7 +128,7 @@ After baking back, the spec line becomes a bounded, testable behavior with accep
135
128
 
136
129
  ## Exit gate
137
130
 
138
- The gate is passed when the spec, constitution and profile were all read (settled questions cited, not re-asked); **every typed point in the handoff has its declared outcome** (asked / validated / left deferred / graduated), with nothing silently dropped; all ten taxonomy categories were considered; only the build-changing gaps were put to the user, as dial-sized decisions with recommendations; every answer is baked into the spec body with observable acceptance criteria, logged under `## Clarifications` and bounded under `## Out of scope`; and the final re-read opened no new gap.
131
+ The gate is passed when the spec, constitution and profile were all read (settled questions cited, not re-asked); **every typed point in the handoff has its declared outcome** (asked / validated / left deferred / graduated), with nothing silently dropped; all ten taxonomy categories were considered; only the build-changing gaps were put to the user, as one ranked batch of decisions with recommendations; every answer is baked into the spec body with observable acceptance criteria, logged under `## Clarifications` and bounded under `## Out of scope`; and the final re-read opened no new gap.
139
132
 
140
133
  ## Result envelope
141
134
 
@@ -165,4 +158,4 @@ Hand off to **`plan`** — turn the now-sharp spec into a technical implementati
165
158
 
166
159
  ## Orientación (siempre)
167
160
 
168
- Cierra cada turno con el **bloque-brújula** (📍 dónde estás · ✅ qué hiciste · 🧭 por qué · ➡️ siguiente, terminando en pregunta), calibrado al dial de `02-DOCS/wiki/harness/user-profile.md`. **Nunca termines en seco.** Protocolo completo: skill `orient` → `skills/orient/references/orientation-contract.md`. (Defiere a `suggest` el "¿instalo la skill que falta?".)
161
+ Habla con la voz de `orient`: frases cortas, una idea por frase, y cada respuesta se entiende sola. Registro técnico o con analogías según `technical_level` en `02-DOCS/wiki/harness/user-profile.md`. Cierra cada turno con el **bloque-brújula** (📍 dónde estás · ➡️ siguiente, terminando en pregunta; ✅ y 🧭 cuando hay algo hecho o decidido). **Nunca termines en seco.** Protocolo completo: skill `orient` → `skills/orient/references/orientation-contract.md`. (Defiere a `suggest` el "¿instalo la skill que falta?".)
@@ -64,8 +64,8 @@ capability:
64
64
  - "Reads the spec, the constitution (02-DOCS/wiki/sdd/constitution.md), and the harness profile first — citing anything the constitution already fixes (e.g. storage) as resolved rather than re-asking it."
65
65
  - "Scans against the ambiguity taxonomy and surfaces concrete gaps: accepted formats, max file size / dimensions, empty/no-photo state, upload-failure recovery, old-file deletion on replace, who-can-edit (actor/permission), and security validation of the file type."
66
66
  - "Ranks the gaps by leverage — asks only the build-changing ones (formats+size, security validation, old-file deletion) and proposes safe defaults for the cosmetic ones instead of burning a question on each."
67
- - "Frames questions as decisions with a recommendation (e.g. 'soft vs hard delete — recommend X because…'), one dial-sized batch, anchored to the spec line, rather than open-ended essay prompts or one-at-a-time drips."
68
- - "Adapts question count to the accompaniment dial from user-profile.md (L0 = only architecture/scope-changing questions + listed assumed-defaults; L3 = broader, taught)."
67
+ - "Frames questions as decisions with a recommendation (e.g. 'soft vs hard delete — recommend X because…'), one ranked batch, anchored to the spec line, rather than open-ended essay prompts or one-at-a-time drips."
68
+ - "Asks only the build-changing questions and lists the rest as assumed-defaults (assumed unless you object), with no verbosity level; phrases them in the register from technical_level in user-profile.md."
69
69
  - "Bakes the resolved answers BACK into the spec: tightens the photo section into bounded behavior, adds observable acceptance criteria, and bounds dropped items under '## Out of scope'."
70
70
  - "Appends a dated '## Clarifications' log of Q → decision → why so the reasoning survives, and does a final re-read to confirm no new gap was opened."
71
71
  - "Stays in its lane: decides WHAT, never proposes the technical HOW (no architecture/library choices) and ends by pointing to plan as the next phase."