shakers-cli 0.15.0 → 0.17.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 (132) hide show
  1. package/README.md +56 -1203
  2. package/bin/ai-usage.js +32 -58
  3. package/bin/login.js +10 -10
  4. package/bin/mcp.js +17 -12
  5. package/bin/mcpb.js +1 -1
  6. package/bin/onboarding.js +3 -2
  7. package/bin/register.js +19 -5
  8. package/bin/report-html.js +2 -2
  9. package/bin/shakers-cli.js +352 -0
  10. package/bin/shakers.js +20 -344
  11. package/bin/share.js +1 -1
  12. package/bin/superadmin.js +5 -5
  13. package/bin/version.js +3 -0
  14. package/package.json +12 -3
  15. package/src/account-marker.js +40 -0
  16. package/src/account-trace.js +23 -0
  17. package/src/agent-category.js +1 -1
  18. package/src/agent-evaluation.js +3 -3
  19. package/src/agent-org-chart.js +5 -5
  20. package/src/agent-synthesis.js +3 -3
  21. package/src/agent-usage.js +1 -1
  22. package/src/agents-client.js +4 -4
  23. package/src/ai-usage-review.js +101 -0
  24. package/src/ai.js +1 -1
  25. package/src/ansi.js +1 -1
  26. package/src/auth-client.js +18 -4
  27. package/src/auth-method-picker.js +1 -1
  28. package/src/auth-methods.js +1 -1
  29. package/src/auth-session-store.js +18 -8
  30. package/src/authorship-mismatch.js +1 -1
  31. package/src/authorship.js +2 -2
  32. package/src/automations-detector.js +1 -1
  33. package/src/backend-request.js +5 -3
  34. package/src/brand-ansi.js +1 -1
  35. package/src/browser-tools-detector.js +1 -1
  36. package/src/certify-disclaimer.js +1 -1
  37. package/src/certify-remediation-prompt.js +1 -1
  38. package/src/cli-args.js +2 -2
  39. package/src/command-audit.js +8 -5
  40. package/src/config-dir.js +1 -1
  41. package/src/config.js +114 -48
  42. package/src/consent-flow.js +7 -6
  43. package/src/consent-skip.js +1 -1
  44. package/src/detection-evidence.js +2 -2
  45. package/src/detection-noise.js +1 -1
  46. package/src/detectors.js +1 -1
  47. package/src/device-login.js +12 -1
  48. package/src/email-verification.js +3 -3
  49. package/src/env-paths.js +1 -1
  50. package/src/find-projects-client.js +12 -0
  51. package/src/find-projects-flow.js +1 -13
  52. package/src/git-activity.js +1 -1
  53. package/src/graph-certs.js +3 -3
  54. package/src/graph-scan.js +9 -9
  55. package/src/i18n.js +148 -138
  56. package/src/interactive-select.js +1 -1
  57. package/src/interview-client.js +6 -1
  58. package/src/interview-transcript.js +1 -1
  59. package/src/legal-notice.js +1 -1
  60. package/src/maturity.js +6 -6
  61. package/src/mcp-ai-usage-result-tool.js +34 -12
  62. package/src/mcp-ai-usage-tool.js +282 -163
  63. package/src/mcp-auth-form.js +7 -3
  64. package/src/mcp-detector.js +3 -3
  65. package/src/mcp-device-login-tools.js +2 -1
  66. package/src/mcp-find-projects-tools.js +1 -2
  67. package/src/mcp-install.js +279 -44
  68. package/src/mcp-instructions.js +1 -1
  69. package/src/mcp-onboarding-tools.js +4 -4
  70. package/src/mcp-prompts.js +4 -4
  71. package/src/mcp-register-tools.js +1 -0
  72. package/src/mcp-role-tools.js +24 -1
  73. package/src/mcp-server.js +5 -1
  74. package/src/mcp-signup-client.js +2 -2
  75. package/src/mcp-signup-tools.js +39 -9
  76. package/src/memory-structure-detector.js +1 -1
  77. package/src/model-call-record.js +2 -2
  78. package/src/onboarding-flow.js +63 -42
  79. package/src/onboarding-funnel.js +153 -20
  80. package/src/open-file.js +1 -1
  81. package/src/osc-link.js +1 -1
  82. package/src/portfolio-client.js +11 -1
  83. package/src/portfolio-git-info.js +1 -1
  84. package/src/profile-flow.js +1 -1
  85. package/src/profile-switch.js +5 -12
  86. package/src/render-certification.js +10 -10
  87. package/src/render-html.js +6 -6
  88. package/src/render-report-text.js +1 -1
  89. package/src/render-sheet.js +10 -10
  90. package/src/render-terminal.js +6 -6
  91. package/src/repo-scope.js +132 -0
  92. package/src/report-gating.js +5 -3
  93. package/src/report-store.js +13 -8
  94. package/src/roadmap-content.js +5 -5
  95. package/src/roadmap-personalization.js +3 -3
  96. package/src/roadmap-prompt.js +2 -2
  97. package/src/roles-flow.js +8 -5
  98. package/src/scan-exclusions.js +1 -1
  99. package/src/scanner.js +14 -14
  100. package/src/secret-prompt.js +1 -1
  101. package/src/session-gate.js +1 -1
  102. package/src/session-refresh.js +33 -3
  103. package/src/session-scan.js +1 -1
  104. package/src/shakers-wordmark.js +1 -1
  105. package/src/share-card.js +2 -2
  106. package/src/share-certification.js +4 -4
  107. package/src/share-consent-state.js +9 -9
  108. package/src/share-payload.js +7 -7
  109. package/src/share.js +7 -7
  110. package/src/signup-client.js +1 -1
  111. package/src/signup-copy.js +177 -2
  112. package/src/skills-client.js +1 -1
  113. package/src/start-add-agents.js +2 -2
  114. package/src/start-add-portfolio.js +3 -13
  115. package/src/start-add-skills.js +2 -2
  116. package/src/stdin-ask.js +16 -4
  117. package/src/strip-internal-comments.js +1 -1
  118. package/src/tech-detector.js +2 -2
  119. package/src/templates/report-sheet.html +11 -13
  120. package/src/terminal-agents.js +8 -8
  121. package/src/terminal-format.js +3 -3
  122. package/src/terminal-sections.js +11 -11
  123. package/src/tier-engine.js +3 -3
  124. package/src/tier-projection.js +2 -2
  125. package/src/transcript-turns.js +1 -1
  126. package/src/typewriter.js +1 -1
  127. package/src/usage-model-calls.js +6 -6
  128. package/src/usage-oneshot-commands.js +5 -5
  129. package/src/usage-repo-scope.js +11 -129
  130. package/src/usage-run-steps.js +82 -181
  131. package/src/usage-scan.js +235 -0
  132. package/src/work-streams.js +1 -1
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // Consent state + email identity + client-side throttle, extracted from src/share.js (structure refactor, issue 020).
3
+ // Consent state + email identity + client-side throttle, extracted from src/share.js.
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
@@ -10,8 +10,8 @@ function configDir() {
10
10
  return getConfigDir(process.env);
11
11
  }
12
12
 
13
- function consentPath() {
14
- return path.join(configDir(), 'consent.json');
13
+ function consentPath(env = process.env) {
14
+ return path.join(getConfigDir(env), 'consent.json');
15
15
  }
16
16
 
17
17
  // Client-side throttle: don't retry a submission if the last one was less than 1h ago.
@@ -19,9 +19,9 @@ const SEND_THROTTLE_MS = 60 * 60 * 1000;
19
19
 
20
20
  // consent state (~/.config/ai-footprint/consent.json) Replaces the old `credentials.json` (token model).
21
21
 
22
- function loadConsentState() {
22
+ function loadConsentState(env = process.env) {
23
23
  try {
24
- return JSON.parse(fs.readFileSync(consentPath(), 'utf8'));
24
+ return JSON.parse(fs.readFileSync(consentPath(env), 'utf8'));
25
25
  } catch {
26
26
  return null;
27
27
  }
@@ -70,7 +70,7 @@ function recordConsent(decision, email = null, { verified } = {}) {
70
70
  state.consent = decision;
71
71
  if (email !== null) state.email = normalizeEmail(email);
72
72
  if (state.lastSentAt === undefined) state.lastSentAt = null;
73
- // Email-ownership verification flag (skill-code-certification / ADR-006).
73
+ // Email-ownership verification flag.
74
74
  if (decision === 'granted' && verified !== undefined) {
75
75
  state.emailVerified = verified === true;
76
76
  }
@@ -78,7 +78,7 @@ function recordConsent(decision, email = null, { verified } = {}) {
78
78
  return state;
79
79
  }
80
80
 
81
- // consent management (issue 007: status / revoke / change email) One-shot actions (do not scan), same pattern the retired `--enroll` used.
81
+ // consent management (status / revoke / change email). One-shot actions (do not scan), same pattern the retired `--enroll` used.
82
82
 
83
83
  // Read-only snapshot for `--consent-status`. Never throws.
84
84
  function getConsentStatus() {
@@ -100,7 +100,7 @@ function revokeConsent() {
100
100
  function resetConsent() {
101
101
  const state = loadConsentState() || {};
102
102
  delete state.consent;
103
- // A re-grant is "concede de cero" (ADR-006): drop the verification flag so the OTP runs again from scratch and nothing is sent under the old email until it's re-verified.
103
+ // A re-grant is "concede de cero": drop the verification flag so the OTP runs again from scratch and nothing is sent under the old email until it's re-verified.
104
104
  delete state.emailVerified;
105
105
  if (state.lastSentAt === undefined) state.lastSentAt = null;
106
106
  saveConsentState(state);
@@ -121,7 +121,7 @@ function setEmail(newEmail) {
121
121
  }
122
122
  const state = loadConsentState() || {};
123
123
  state.email = normalizeEmail(newEmail);
124
- // A new email is unverified until proven (ADR-006): reset the flag so
124
+ // A new email is unverified until proven: reset the flag so
125
125
  // autoShare won't send under an address whose ownership wasn't checked.
126
126
  state.emailVerified = false;
127
127
  saveConsentState(state);
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // The derived-payload whitelist for the `usage/reports` ingest, extracted from src/share.js (structure refactor, issue 020).
3
+ // The derived-payload whitelist for the `usage/reports` ingest, extracted from src/share.js.
4
4
 
5
5
  const { scrubSecrets } = require('./agent-synthesis');
6
6
  const { MAX_DEFINITION_CHARS } = require('./agent-evaluation');
@@ -13,7 +13,7 @@ function whitelistSchedulerProbe(probe) {
13
13
  };
14
14
  }
15
15
 
16
- // WHY ONE MORE FIELD IS ADMISSIBLE ON THE AGENT WHITELIST (issue 115).
16
+ // WHY ONE MORE FIELD IS ADMISSIBLE ON THE AGENT WHITELIST.
17
17
  function agentCatalogIdsByName(report) {
18
18
  const evaluations =
19
19
  report.agentEvaluation && Array.isArray(report.agentEvaluation.evaluations)
@@ -86,7 +86,7 @@ function derivePayload(report, maturity) {
86
86
  tools: Array.isArray(a.tools) ? a.tools : [],
87
87
  model: a.model || null,
88
88
  parent: a.parent || null,
89
- // issue 115 — the ONLY classification field that travels.
89
+ // The ONLY classification field that travels.
90
90
  catalogId: catalogIdsByName.get(a.name) || null,
91
91
  // agent-classifier fix — the agent's own definition (frontmatter description + body), scrubbed + capped.
92
92
  ...(definitionsByName.get(a.name)
@@ -132,14 +132,14 @@ function derivePayload(report, maturity) {
132
132
  mcp: Array.isArray(browserToolsVia.mcp) && browserToolsVia.mcp.length > 0,
133
133
  },
134
134
  },
135
- // Hybrid evaluation, Slice 1: two DETERMINISTIC behavioural blocks (no LLM).
135
+ // Two DETERMINISTIC behavioural blocks (no LLM).
136
136
  gitActivity: whitelistGitActivity(report.gitActivity),
137
137
  sessions: whitelistSessions(report.sessions),
138
- // Hybrid evaluation, Slice 2: one DETERMINISTIC block (workStreams) and two REDACTED-excerpt blocks (steering, decisions).
138
+ // One DETERMINISTIC block (workStreams) and two REDACTED-excerpt blocks (steering, decisions).
139
139
  workStreams: whitelistWorkStreams(report.workStreams),
140
140
  steering: whitelistSteering(report.steering),
141
141
  decisions: whitelistDecisions(report.decisions),
142
- // Hybrid evaluation, Slice 3c: PER-CANDIDATE authorship + depth (per detected skill, per detected agent).
142
+ // PER-CANDIDATE authorship + depth (per detected skill, per detected agent).
143
143
  detectionEvidence: whitelistDetectionEvidence(report.detectionEvidence),
144
144
  };
145
145
  }
@@ -176,7 +176,7 @@ function whitelistDetectionEvidence(evidence) {
176
176
  return { skills, agents, provenance: whitelistProvenance(evidence.provenance) };
177
177
  }
178
178
 
179
- // Re-derives the repo-level anti-noise `provenance` (Slice 3): the (weak) fork signal and the (conservative) boilerplate/tutorial signal.
179
+ // Re-derives the repo-level anti-noise `provenance`: the (weak) fork signal and the (conservative) boilerplate/tutorial signal.
180
180
  function whitelistProvenance(provenance) {
181
181
  if (!provenance || typeof provenance !== 'object') return null;
182
182
  const marker =
package/src/share.js CHANGED
@@ -33,7 +33,7 @@ const {
33
33
 
34
34
  /* ---------- minimal HTTP utility (no dependencies) ---------- */
35
35
 
36
- // POSTs `body` to the ONE backend (talents-ai-score, ADR-042 — the PRIMARY -> FALLBACK chain is retired), via src/backend-request.js.
36
+ // POSTs `body` to the ONE backend (the PRIMARY -> FALLBACK chain is retired), via src/backend-request.js.
37
37
  async function requestJsonBackend(endpoint, body, { timeoutMs = 15000, idempotencyKey = null } = {}) {
38
38
  const { status, raw, backend } = await requestBackend({ endpoint, body, timeoutMs, idempotencyKey });
39
39
  let json = null;
@@ -53,7 +53,7 @@ async function autoShare(report, maturity, { root, bypassThrottle = false } = {}
53
53
  if (!state.email) {
54
54
  return { ok: false, skipped: true, reason: 'no-email' };
55
55
  }
56
- // ADR-006: never SEND under an email whose ownership wasn't proven — every
56
+ // Never SEND under an email whose ownership wasn't proven — every
57
57
  // profile, `external` included (legal requirement), must have verified.
58
58
  if (state.emailVerified === false) {
59
59
  return { ok: false, skipped: true, reason: 'email-unverified' };
@@ -92,14 +92,14 @@ async function autoShare(report, maturity, { root, bypassThrottle = false } = {}
92
92
  return { ok: false, skipped: false, reason: 'rate-limited' };
93
93
  }
94
94
  if (res.status === 503) {
95
- // Generic "service unavailable" from the server (ADR-011 retires the
96
- // server-side kill switch that used to be the main 503 cause).
95
+ // Generic "service unavailable" from the server (the retired
96
+ // server-side kill switch used to be the main 503 cause).
97
97
  return { ok: false, skipped: false, reason: 'service-unavailable' };
98
98
  }
99
99
  return { ok: false, skipped: false, reason: `http-${res.status}` };
100
100
  }
101
101
 
102
- /* ---------- certification persistence (skill-code-certification, issue 005) ---------- */
102
+ /* ---------- certification persistence ---------- */
103
103
 
104
104
  // Persists the analyzed certification result if (and only if) consent is granted.
105
105
  async function shareCertification(items, { repository = null, commitRange = null, toolVersion = null, superadminToken = null, root = null } = {}) {
@@ -112,7 +112,7 @@ async function shareCertification(items, { repository = null, commitRange = null
112
112
  if (!state.email) {
113
113
  return { ok: false, skipped: true, reason: 'no-email' };
114
114
  }
115
- // ADR-006 (mirror of autoShare): don't persist under an unverified email.
115
+ // Don't persist under an unverified email.
116
116
  if (state.emailVerified === false) {
117
117
  return { ok: false, skipped: true, reason: 'email-unverified' };
118
118
  }
@@ -132,7 +132,7 @@ async function shareCertification(items, { repository = null, commitRange = null
132
132
 
133
133
  let res;
134
134
  try {
135
- // ADR-027: forward the superadmin session token so the backend stamps test_origin=true (server-authoritative, non-prod only).
135
+ // Forward the superadmin session token so the backend stamps test_origin=true (server-authoritative, non-prod only).
136
136
  const body = { email: state.email, payload };
137
137
  if (superadminToken) body.superadminToken = superadminToken;
138
138
  // Idempotency-Key (three-way security review, cross-repo): ONE key per
@@ -4,7 +4,7 @@ const { postJsonWithTimeout, extractSetCookie } = require('./backend-request');
4
4
  const { SESSION_COOKIE_NAME } = require('./auth-client');
5
5
  const { toUpperLang } = require('./lang-codes');
6
6
 
7
- // Register a talent via the Hub's better-auth email engine, in two steps (owner decision, supersedes ADR-031 for the email path — WEB-login parity): 1.
7
+ // Register a talent via the Hub's better-auth email engine, in two steps (WEB-login parity): 1.
8
8
 
9
9
  const DEFAULT_TIMEOUT_MS = 30000;
10
10
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // Talent-facing copy of the MCP sign-up (ticket 39540, UX review 39574): fixed text the AI shows word for word, in the talent's language.
3
+ // Talent-facing copy of the MCP sign-up: fixed text the AI shows word for word, in the talent's language.
4
4
 
5
5
  const SIGNUP_LANGUAGES = ['es', 'en', 'it', 'pt'];
6
6
 
@@ -405,6 +405,177 @@ const COPY = {
405
405
  },
406
406
  };
407
407
 
408
+ const REVIEW = {
409
+ es: {
410
+ cliQuestion: '¿Lo subo a tu perfil? [s/N]:',
411
+ cliIncludeExcerpts: '¿Incluir los extractos de texto? [S/n]:',
412
+ cliExcludeAgents: 'Si quieres dejar fuera algún agente, escribe sus nombres separados por comas (Enter para ninguno):',
413
+ cliUnknownAgents: (unknown, known) => `No reconozco: ${unknown}. Los agentes son: ${known}.`,
414
+ cliRepoHint: 'Para dejar fuera un repositorio, vuelve a ejecutar el comando eligiendo solo los que quieras con --repos.',
415
+ title: 'Todavía no se ha subido nada a tu perfil de Shakers. Esto es lo que se subiría:',
416
+ sentTitle: 'Esto es lo que se ha subido:',
417
+ tools: (list) => `Herramientas de IA detectadas: ${list}.`,
418
+ technologies: (list) => `Tecnologías de tus proyectos: ${list}.`,
419
+ activity: (sessions, days) => `Tu actividad con IA: ${sessions === 1 ? '1 sesión' : `${sessions} sesiones`}${days === null ? '' : `, unos ${days} días activos por semana`}.`,
420
+ repos: (list, commits) => `Repositorios analizados: ${list}. De ellos solo se envían cifras (${commits === 1 ? '1 commit' : `${commits} commits`}, líneas cambiadas, tipos de fichero), nunca su nombre ni su código.`,
421
+ agents: (list, definitions, summaries) => `Agentes de IA detectados: ${list}. De cada uno se envía ${listOf('es', ['su nombre', 'sus herramientas', 'su modelo', 'de qué agente depende', ...(definitions > 0 ? ['el texto de su definición (sin secretos)'] : []), ...(summaries > 0 ? ['el resumen de lo que hace'] : [])])}.`,
422
+ excerpts: (conversation, commands) => `Extractos de texto: ${conversation} frases cortas de lo que le pides a la IA y ${commands} comandos, de 200 caracteres como máximo, sin secretos, rutas ni emails.`,
423
+ noExcerpts: 'Extractos de texto: ninguno.',
424
+ notSent: 'No se envía: tu código, tus conversaciones completas ni los nombres de tus repositorios.',
425
+ explanationTitle: 'Qué hace Shakers con esto:',
426
+ explanation: [
427
+ 'Te sitúa en dos ejes. Configuración: qué tienes montado (herramientas, servidores MCP, agentes), de menos a más: Asistido, Extendido u Orquestado. Uso: cómo diriges a la IA (si le das instrucciones concretas, si revisas lo que propone), de menos a más: Reactivo, Deliberado o Riguroso.',
428
+ 'La configuración se calcula con reglas fijas a partir de lo detectado. El nivel de uso y los textos sobre cómo trabajas los escribe un modelo de IA en los servidores de Shakers.',
429
+ 'En tu perfil aparece la sección «Mi trabajo con IA»: tus dos niveles, tu casilla en la matriz (por ejemplo Practitioner o Architect), tus herramientas y agentes, y dos textos que puedes editar en la web.',
430
+ ],
431
+ afterwards: 'Si no estás de acuerdo con el resultado, pulsa «Solicitar revisión humana» en esa sección de la web o escribe a talent@shakersworks.com. Hoy no puedes ocultar la sección tú mismo; para pedir que se borre, escribe a ese mismo correo.',
432
+ excludeHint: 'Si quieres dejar fuera algún repositorio, algún agente o los extractos de texto, dímelo al responder.',
433
+ question: '¿Lo subo a tu perfil?',
434
+ yes: 'Subir a mi perfil',
435
+ no: 'No subir',
436
+ formExcerpts: 'Incluir los extractos de texto',
437
+ formRepo: (name) => `Incluir el repositorio ${name}`,
438
+ formAgent: (name) => `Incluir el agente ${name}`,
439
+ declined: 'No se ha subido nada. El análisis se queda solo en tu equipo.',
440
+ nothingLeft: 'Has dejado fuera todos los repositorios, así que no se ha subido nada.',
441
+ throttled: 'Ya subiste un análisis hace menos de una hora, así que no se ha subido nada. Puedes volver a intentarlo más tarde.',
442
+ uploaded: 'Subido a tu perfil. Shakers lo está evaluando y el resultado tarda unos minutos.',
443
+ uploadFailed: 'No se ha podido subir. Tu análisis se queda solo en tu equipo; puedes volver a intentarlo más tarde.',
444
+ resultTitle: 'Tu resultado en «Mi trabajo con IA»:',
445
+ setupLevel: (level) => `Configuración: ${level}.`,
446
+ usageLevel: (level) => `Uso: ${level}.`,
447
+ cell: (name) => `Tu casilla en la matriz: ${name}.`,
448
+ levels: { ASSISTED: 'Asistido', EXTENDED: 'Extendido', ORCHESTRATED: 'Orquestado', REACTIVE: 'Reactivo', DELIBERATE: 'Deliberado', RIGOROUS: 'Riguroso' },
449
+ resultPending: 'Tu resultado todavía no está en «Mi trabajo con IA». Si acabas de subir tu análisis, aparecerá en unos minutos.',
450
+ },
451
+ en: {
452
+ cliQuestion: 'Shall I upload it to your profile? [y/N]:',
453
+ cliIncludeExcerpts: 'Include the text excerpts? [Y/n]:',
454
+ cliExcludeAgents: 'If you want to leave out an agent, type their names separated by commas (Enter for none):',
455
+ cliUnknownAgents: (unknown, known) => `I do not recognise: ${unknown}. The agents are: ${known}.`,
456
+ cliRepoHint: 'To leave out a repository, run the command again choosing only the ones you want with --repos.',
457
+ title: 'Nothing has been uploaded to your Shakers profile yet. This is what would be uploaded:',
458
+ sentTitle: 'This is what was uploaded:',
459
+ tools: (list) => `AI tools detected: ${list}.`,
460
+ technologies: (list) => `Technologies in your projects: ${list}.`,
461
+ activity: (sessions, days) => `Your AI activity: ${sessions === 1 ? '1 session' : `${sessions} sessions`}${days === null ? '' : `, about ${days} active days a week`}.`,
462
+ repos: (list, commits) => `Repositories analysed: ${list}. Only figures about them are sent (${commits === 1 ? '1 commit' : `${commits} commits`}, lines changed, file types), never their name or their code.`,
463
+ agents: (list, definitions, summaries) => `AI agents detected: ${list}. For each one we send ${listOf('en', ['its name', 'its tools', 'its model', 'which agent it reports to', ...(definitions > 0 ? ['the text of its definition (with secrets removed)'] : []), ...(summaries > 0 ? ['the summary of what it does'] : [])])}.`,
464
+ excerpts: (conversation, commands) => `Text excerpts: ${conversation} short sentences of what you ask the AI and ${commands} commands, 200 characters at most, with secrets, paths and emails removed.`,
465
+ noExcerpts: 'Text excerpts: none.',
466
+ notSent: 'Not sent: your code, your full conversations or the names of your repositories.',
467
+ explanationTitle: 'What Shakers does with it:',
468
+ explanation: [
469
+ 'It places you on two axes. Setup: what you have in place (tools, MCP servers, agents), from less to more: Assisted, Extended or Orchestrated. Usage: how you steer the AI (whether you give it concrete instructions, whether you review what it proposes), from less to more: Reactive, Deliberate or Rigorous.',
470
+ 'Setup is worked out with fixed rules from what was detected. The usage level and the texts about how you work are written by an AI model on Shakers servers.',
471
+ 'Your profile shows the "My work with AI" section: your two levels, your square in the matrix (for example Practitioner or Architect), your tools and agents, and two texts you can edit on the web.',
472
+ ],
473
+ afterwards: 'If you disagree with the result, press "Request human review" in that section on the web or write to talent@shakersworks.com. Today you cannot hide the section yourself; to ask for it to be deleted, write to that same address.',
474
+ excludeHint: 'If you want to leave out a repository, an agent or the text excerpts, tell me when you answer.',
475
+ question: 'Shall I upload it to your profile?',
476
+ yes: 'Upload to my profile',
477
+ no: 'Do not upload',
478
+ formExcerpts: 'Include the text excerpts',
479
+ formRepo: (name) => `Include the repository ${name}`,
480
+ formAgent: (name) => `Include the agent ${name}`,
481
+ declined: 'Nothing was uploaded. The analysis stays on your computer only.',
482
+ nothingLeft: 'You left out every repository, so nothing was uploaded.',
483
+ throttled: 'You uploaded an analysis less than an hour ago, so nothing was uploaded. You can try again later.',
484
+ uploaded: 'Uploaded to your profile. Shakers is evaluating it and the result takes a few minutes.',
485
+ uploadFailed: 'It could not be uploaded. Your analysis stays on your computer only; you can try again later.',
486
+ resultTitle: 'Your result in "My work with AI":',
487
+ setupLevel: (level) => `Setup: ${level}.`,
488
+ usageLevel: (level) => `Usage: ${level}.`,
489
+ cell: (name) => `Your square in the matrix: ${name}.`,
490
+ levels: { ASSISTED: 'Assisted', EXTENDED: 'Extended', ORCHESTRATED: 'Orchestrated', REACTIVE: 'Reactive', DELIBERATE: 'Deliberate', RIGOROUS: 'Rigorous' },
491
+ resultPending: 'Your result is not in "My work with AI" yet. If you have just uploaded your analysis, it will show in a few minutes.',
492
+ },
493
+ it: {
494
+ cliQuestion: 'Lo carico sul tuo profilo? [s/N]:',
495
+ cliIncludeExcerpts: 'Includere gli estratti di testo? [S/n]:',
496
+ cliExcludeAgents: 'Se vuoi escludere un agente, scrivi i nomi separati da virgole (Invio per nessuno):',
497
+ cliUnknownAgents: (unknown, known) => `Non riconosco: ${unknown}. Gli agenti sono: ${known}.`,
498
+ cliRepoHint: 'Per escludere un repository, esegui di nuovo il comando scegliendo solo quelli che vuoi con --repos.',
499
+ title: "Non è stato ancora caricato nulla sul tuo profilo Shakers. Ecco cosa verrebbe caricato:",
500
+ sentTitle: "Ecco cosa è stato caricato:",
501
+ tools: (list) => `Strumenti di IA rilevati: ${list}.`,
502
+ technologies: (list) => `Tecnologie dei tuoi progetti: ${list}.`,
503
+ activity: (sessions, days) => `La tua attività con l'IA: ${sessions === 1 ? '1 sessione' : `${sessions} sessioni`}${days === null ? '' : `, circa ${days} giorni attivi a settimana`}.`,
504
+ repos: (list, commits) => `Repository analizzati: ${list}. Di questi si inviano solo cifre (${commits} commit, righe modificate, tipi di file), mai il nome né il codice.`,
505
+ agents: (list, definitions, summaries) => `Agenti di IA rilevati: ${list}. Di ognuno si inviano ${listOf('it', ['il nome', 'gli strumenti', 'il modello', 'da quale agente dipende', ...(definitions > 0 ? ['il testo della sua definizione (senza segreti)'] : []), ...(summaries > 0 ? ['il riassunto di ciò che fa'] : [])])}.`,
506
+ excerpts: (conversation, commands) => `Estratti di testo: ${conversation} frasi brevi di ciò che chiedi all'IA e ${commands} comandi, al massimo 200 caratteri, senza segreti, percorsi né email.`,
507
+ noExcerpts: 'Estratti di testo: nessuno.',
508
+ notSent: 'Non si inviano: il tuo codice, le tue conversazioni complete né i nomi dei tuoi repository.',
509
+ explanationTitle: 'Cosa fa Shakers con questi dati:',
510
+ explanation: [
511
+ "Ti colloca su due assi. Configurazione: cosa hai impostato (strumenti, server MCP, agenti), dal meno al più: Assistito, Esteso o Orchestrato. Uso: come guidi l'IA (se le dai istruzioni concrete, se controlli ciò che propone), dal meno al più: Reattivo, Deliberato o Rigoroso.",
512
+ "La configurazione si calcola con regole fisse da ciò che è stato rilevato. Il livello di uso e i testi su come lavori li scrive un modello di IA sui server di Shakers.",
513
+ "Sul tuo profilo compare la sezione «Il mio lavoro con l'IA»: i tuoi due livelli, la tua casella nella matrice (per esempio Practitioner o Architect), i tuoi strumenti e agenti, e due testi che puoi modificare sul sito.",
514
+ ],
515
+ afterwards: 'Se non sei d\'accordo con il risultato, premi «Richiedi una revisione umana» in quella sezione del sito o scrivi a talent@shakersworks.com. Oggi non puoi nascondere la sezione da solo; per chiedere che venga cancellata, scrivi allo stesso indirizzo.',
516
+ excludeHint: 'Se vuoi escludere un repository, un agente o gli estratti di testo, dimmelo quando rispondi.',
517
+ question: 'Lo carico sul tuo profilo?',
518
+ yes: 'Carica sul mio profilo',
519
+ no: 'Non caricare',
520
+ formExcerpts: 'Includi gli estratti di testo',
521
+ formRepo: (name) => `Includi il repository ${name}`,
522
+ formAgent: (name) => `Includi l'agente ${name}`,
523
+ declined: "Non è stato caricato nulla. L'analisi resta solo sul tuo computer.",
524
+ nothingLeft: 'Hai escluso tutti i repository, quindi non è stato caricato nulla.',
525
+ throttled: "Hai caricato un'analisi meno di un'ora fa, quindi non è stato caricato nulla. Puoi riprovare più tardi.",
526
+ uploaded: 'Caricato sul tuo profilo. Shakers lo sta valutando e il risultato richiede qualche minuto.',
527
+ uploadFailed: 'Non è stato possibile caricarlo. La tua analisi resta solo sul tuo computer; puoi riprovare più tardi.',
528
+ resultTitle: "Il tuo risultato in «Il mio lavoro con l'IA»:",
529
+ setupLevel: (level) => `Configurazione: ${level}.`,
530
+ usageLevel: (level) => `Uso: ${level}.`,
531
+ cell: (name) => `La tua casella nella matrice: ${name}.`,
532
+ levels: { ASSISTED: 'Assistito', EXTENDED: 'Esteso', ORCHESTRATED: 'Orchestrato', REACTIVE: 'Reattivo', DELIBERATE: 'Deliberato', RIGOROUS: 'Rigoroso' },
533
+ resultPending: "Il tuo risultato non è ancora in «Il mio lavoro con l'IA». Se hai appena caricato la tua analisi, comparirà tra qualche minuto.",
534
+ },
535
+ pt: {
536
+ cliQuestion: 'Carrego-o no seu perfil? [s/N]:',
537
+ cliIncludeExcerpts: 'Incluir os excertos de texto? [S/n]:',
538
+ cliExcludeAgents: 'Se quiser deixar de fora algum agente, escreva os nomes separados por vírgulas (Enter para nenhum):',
539
+ cliUnknownAgents: (unknown, known) => `Não reconheço: ${unknown}. Os agentes são: ${known}.`,
540
+ cliRepoHint: 'Para deixar de fora um repositório, volte a executar o comando escolhendo só os que quiser com --repos.',
541
+ title: 'Ainda não foi carregado nada no seu perfil da Shakers. Isto é o que seria carregado:',
542
+ sentTitle: 'Isto é o que foi carregado:',
543
+ tools: (list) => `Ferramentas de IA detetadas: ${list}.`,
544
+ technologies: (list) => `Tecnologias dos seus projetos: ${list}.`,
545
+ activity: (sessions, days) => `A sua atividade com IA: ${sessions === 1 ? '1 sessão' : `${sessions} sessões`}${days === null ? '' : `, cerca de ${days} dias ativos por semana`}.`,
546
+ repos: (list, commits) => `Repositórios analisados: ${list}. Deles só se enviam números (${commits === 1 ? '1 commit' : `${commits} commits`}, linhas alteradas, tipos de ficheiro), nunca o nome nem o código.`,
547
+ agents: (list, definitions, summaries) => `Agentes de IA detetados: ${list}. De cada um envia-se ${listOf('pt', ['o nome', 'as ferramentas', 'o modelo', 'de que agente depende', ...(definitions > 0 ? ['o texto da sua definição (sem segredos)'] : []), ...(summaries > 0 ? ['o resumo do que faz'] : [])])}.`,
548
+ excerpts: (conversation, commands) => `Excertos de texto: ${conversation} frases curtas do que pede à IA e ${commands} comandos, com 200 caracteres no máximo, sem segredos, caminhos nem emails.`,
549
+ noExcerpts: 'Excertos de texto: nenhum.',
550
+ notSent: 'Não se envia: o seu código, as suas conversas completas nem os nomes dos seus repositórios.',
551
+ explanationTitle: 'O que a Shakers faz com isto:',
552
+ explanation: [
553
+ 'Situa-o em dois eixos. Configuração: o que tem montado (ferramentas, servidores MCP, agentes), de menos para mais: Assistido, Estendido ou Orquestrado. Uso: como orienta a IA (se lhe dá instruções concretas, se revê o que propõe), de menos para mais: Reativo, Deliberado ou Rigoroso.',
554
+ 'A configuração calcula-se com regras fixas a partir do que foi detetado. O nível de uso e os textos sobre como trabalha são escritos por um modelo de IA nos servidores da Shakers.',
555
+ 'No seu perfil aparece a secção «Meu trabalho com IA»: os seus dois níveis, a sua casa na matriz (por exemplo Practitioner ou Architect), as suas ferramentas e agentes, e dois textos que pode editar na web.',
556
+ ],
557
+ afterwards: 'Se não concordar com o resultado, carregue em «Solicitar revisão humana» nessa secção da web ou escreva para talent@shakersworks.com. Hoje não pode ocultar a secção sozinho; para pedir que seja apagada, escreva para esse mesmo endereço.',
558
+ excludeHint: 'Se quiser deixar de fora algum repositório, algum agente ou os excertos de texto, diga-me ao responder.',
559
+ question: 'Carrego-o no seu perfil?',
560
+ yes: 'Carregar no meu perfil',
561
+ no: 'Não carregar',
562
+ formExcerpts: 'Incluir os excertos de texto',
563
+ formRepo: (name) => `Incluir o repositório ${name}`,
564
+ formAgent: (name) => `Incluir o agente ${name}`,
565
+ declined: 'Não foi carregado nada. A análise fica só no seu computador.',
566
+ nothingLeft: 'Deixou de fora todos os repositórios, por isso não foi carregado nada.',
567
+ throttled: 'Carregou uma análise há menos de uma hora, por isso não foi carregado nada. Pode tentar mais tarde.',
568
+ uploaded: 'Carregado no seu perfil. A Shakers está a avaliá-lo e o resultado demora alguns minutos.',
569
+ uploadFailed: 'Não foi possível carregá-lo. A sua análise fica só no seu computador; pode tentar mais tarde.',
570
+ resultTitle: 'O seu resultado em «Meu trabalho com IA»:',
571
+ setupLevel: (level) => `Configuração: ${level}.`,
572
+ usageLevel: (level) => `Uso: ${level}.`,
573
+ cell: (name) => `A sua casa na matriz: ${name}.`,
574
+ levels: { ASSISTED: 'Assistido', EXTENDED: 'Estendido', ORCHESTRATED: 'Orquestrado', REACTIVE: 'Reativo', DELIBERATE: 'Deliberado', RIGOROUS: 'Rigoroso' },
575
+ resultPending: 'O seu resultado ainda não está em «Meu trabalho com IA». Se acabou de carregar a sua análise, vai aparecer dentro de alguns minutos.',
576
+ },
577
+ };
578
+
408
579
  function toSignupLanguage(code) {
409
580
  const base = String(code || '').toLowerCase().split(/[-_]/)[0];
410
581
  return SIGNUP_LANGUAGES.includes(base) ? base : null;
@@ -418,6 +589,10 @@ function legalCopy(lang) {
418
589
  return LEGAL[toSignupLanguage(lang) || 'en'];
419
590
  }
420
591
 
592
+ function reviewCopy(lang) {
593
+ return REVIEW[toSignupLanguage(lang) || 'en'];
594
+ }
595
+
421
596
  // The profile draft the talent confirms before the account exists: one line per group, empty parts left out.
422
597
  function renderDraft(lang, d = {}) {
423
598
  const c = signupCopy(lang);
@@ -447,4 +622,4 @@ function renderRole(lang, { name, source, evidence, openProjects, recommended })
447
622
  ].filter(Boolean).join('\n');
448
623
  }
449
624
 
450
- module.exports = { SIGNUP_LANGUAGES, LEGAL, signupCopy, legalCopy, toSignupLanguage, renderDraft, renderRole, amount };
625
+ module.exports = { SIGNUP_LANGUAGES, LEGAL, REVIEW, signupCopy, legalCopy, reviewCopy, toSignupLanguage, renderDraft, renderRole, amount };
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { postJsonWithTimeout } = require('./backend-request');
4
4
 
5
- // Talent-skills client (talents-ai-score, ADR-054/055 — `start`'s "Añadir skills" route, src/start-add-skills.js).
5
+ // Talent-skills client (`start`'s "Añadir skills" route, src/start-add-skills.js).
6
6
 
7
7
  const DEFAULT_TIMEOUT_MS = 20000;
8
8
 
@@ -24,7 +24,7 @@ const { reasonCopy, pickPortfolios } = require('./start-add-skills');
24
24
  const { askWithDefault } = require('./start-add-portfolio');
25
25
  const { styleQuestion, styleSuccess } = require('./ansi');
26
26
 
27
- // `start`'s "Add agent to profile" route (talents-ai-score Phase 2), mirror of src/start-add-portfolio.js: the IA pre-drafts, the Talent edits.
27
+ // `start`'s "Add agent to profile" route, mirror of src/start-add-portfolio.js: the IA pre-drafts, the Talent edits.
28
28
 
29
29
  const MAX_DEFINITION_CHARS = 4000;
30
30
 
@@ -68,7 +68,7 @@ async function chooseAgent({ ask, stdinIsTTY, agents, c, input, output }) {
68
68
  return idx === null ? null : agents[idx];
69
69
  }
70
70
 
71
- // Step 4 — the two AI-drafted fields, consent-gated (ADR-052) before any egress.
71
+ // Step 4 — the two AI-drafted fields, consent-gated before any egress.
72
72
  async function resolveAgentFields({ ask, catalog, c, stdinIsTTY, agent, accessToken, preAccepted = false }) {
73
73
  const freeText = async () => ({
74
74
  whatItDoes: await askFreeText(ask, c.agentWhatItDoesFreeTextPrompt),
@@ -8,7 +8,7 @@ const {
8
8
  getPortfoliosListEndpoint, getPortfoliosDeclareEndpoint, getPortfolioDraftDescriptionEndpoint,
9
9
  getSkillsResolveMatchedEndpoint, getSkillsResolveAddableEndpoint, getSkillsDeclareEndpoint,
10
10
  } = require('./config');
11
- const { requestListPortfolios, requestDeclarePortfolio, requestDraftDescription } = require('./portfolio-client');
11
+ const { requestListPortfolios, requestDeclarePortfolio, requestDraftDescription, normalizeDomain } = require('./portfolio-client');
12
12
  const { requestResolveAddableSkills: requestResolveMatchedSkills, requestResolveAddableSkills } = require('./skills-client');
13
13
  const { getRemoteUrl, getCommitDateRange } = require('./portfolio-git-info');
14
14
  const { confirmDisclaimerAcceptance } = require('./certify-disclaimer');
@@ -21,7 +21,7 @@ const { palette, styleQuestion, styleSuccess } = require('./ansi');
21
21
  // Only `dim`/`reset` for the numbered fallback's descriptions — the exact treatment bin/start.js's own menu and bin/certify.js's picker give theirs.
22
22
  const ANSI = palette({ reset: '\x1b[0m', dim: '\x1b[2m' });
23
23
 
24
- // `start`'s "Add project to portfolio" route (talents-ai-score, ADR-059).
24
+ // `start`'s "Add project to portfolio" route.
25
25
 
26
26
  const MAX_README_CHARS = 4000;
27
27
  const MAX_PACKAGE_DESCRIPTION_CHARS = 500;
@@ -56,15 +56,6 @@ async function askFreeText(ask, promptText) {
56
56
  return String(await ask(styleQuestion(promptText))).trim();
57
57
  }
58
58
 
59
- function normalizeDomain(raw) {
60
- let s = String(raw || '').trim().toLowerCase();
61
- if (!s) return '';
62
- s = s.replace(/^https?:\/\//, '');
63
- s = s.split('/')[0].split('?')[0].split('#')[0];
64
- s = s.replace(/^www\./, '');
65
- return s;
66
- }
67
-
68
59
  // Step 2 — Type.
69
60
  function typeOptions(c) {
70
61
  return [
@@ -345,7 +336,7 @@ async function runAddPortfolio({
345
336
 
346
337
  if (declared.ok) {
347
338
  process.stdout.write(`\n ${styleSuccess(c.portfolioDeclaredSuccess(title))}\n\n`);
348
- // OPT-IN (dueño, 2026-08-12): offer the REST of the detected skills for the Talent's PROFILE, now that the portfolio itself (and whatever skillIds it carried) is safely declared.
339
+ // OPT-IN: offer the REST of the detected skills for the Talent's PROFILE, now that the portfolio itself (and whatever skillIds it carried) is safely declared.
349
340
  await offerAddableSkillsForProfile({ ask, catalog, c, technologies, session, stdinIsTTY, input, output, runLogin });
350
341
  return;
351
342
  }
@@ -383,6 +374,5 @@ module.exports = {
383
374
  askWithDefault,
384
375
  chooseType,
385
376
  typeOptions,
386
- normalizeDomain,
387
377
  offerAddableSkillsForProfile,
388
378
  };
@@ -9,7 +9,7 @@ const { runInteractiveMultiSelect } = require('./interactive-select');
9
9
  const { loadAuthSession, sessionStatus } = require('./auth-session-store');
10
10
  const { styleSuccess } = require('./ansi');
11
11
 
12
- // `start`'s "Añadir skills" route (talents-ai-score, ADR-054/055).
12
+ // `start`'s "Añadir skills" route.
13
13
 
14
14
  // Maps a technical failure reason to the localized `start.errorReason` copy.
15
15
  function reasonCopy(reason, c) {
@@ -255,6 +255,6 @@ module.exports = {
255
255
  declareAll,
256
256
  printProfileBoost,
257
257
  offerRelateToPortfolios,
258
- // Exported for src/start-add-agents.js's own relate step (ADR-041): the multi-select over the Talent's portfolios, reused verbatim.
258
+ // Exported for src/start-add-agents.js's own relate step: the multi-select over the Talent's portfolios, reused verbatim.
259
259
  pickPortfolios,
260
260
  };
package/src/stdin-ask.js CHANGED
@@ -49,14 +49,14 @@ function createLineQueueAsk(writeQuestion) {
49
49
 
50
50
  // `onInterrupt` (optional): a Ctrl-C (SIGINT) handler for a TTY.
51
51
  function createStdinAsk({ onInterrupt } = {}) {
52
- // `rl` is re-createable (issue 102), not a `const`.
52
+ // `rl` is re-createable, not a `const`.
53
53
  let rl = null;
54
54
  let ended = false;
55
55
  let suspending = false;
56
56
  let currentPrompt = '';
57
57
 
58
58
  const { ask, pushLine, markEnded, drain } = createLineQueueAsk((question) => {
59
- // Long-answer wrap bug (skill-code-certification, certify agents): in a TTY, `readline` runs in terminal mode and redraws the current line on every keystroke (wrap, cursor).
59
+ // Long-answer wrap bug (certify agents): in a TTY, `readline` runs in terminal mode and redraws the current line on every keystroke (wrap, cursor).
60
60
  const text = ` ${question} `;
61
61
  const lastNewline = text.lastIndexOf('\n');
62
62
  if (lastNewline >= 0) {
@@ -98,7 +98,7 @@ function createStdinAsk({ onInterrupt } = {}) {
98
98
  r.close();
99
99
  suspending = false;
100
100
  }
101
- // SECRETS DO NOT ECHO (issue 102).
101
+ // SECRETS DO NOT ECHO.
102
102
  ask.secret = async (question) => {
103
103
  if (!canPromptSecretly(process.stdin)) return ask(question);
104
104
  process.stdout.write(` ${question} `);
@@ -126,4 +126,16 @@ function createStdinAsk({ onInterrupt } = {}) {
126
126
  return ask;
127
127
  }
128
128
 
129
- module.exports = { createStdinAsk, createLineQueueAsk };
129
+ function createLazyStdinAsk() {
130
+ let real = null;
131
+ const open = () => { real = real || createStdinAsk(); return real; };
132
+ const ask = (question) => open()(question);
133
+ ask.secret = (question) => open().secret(question);
134
+ ask.drain = () => { if (real) real.drain(); };
135
+ ask.close = () => { if (real) real.close(); };
136
+ ask.suspend = () => { if (real) real.suspend(); };
137
+ ask.resume = () => { if (real) real.resume(); };
138
+ return ask;
139
+ }
140
+
141
+ module.exports = { createStdinAsk, createLazyStdinAsk, createLineQueueAsk };
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // Our comments do not travel in the talent's shareable report (issue 098).
3
+ // Our comments do not travel in the talent's shareable report.
4
4
 
5
5
  // `<!-- … -->`, including multi-line.
6
6
  const HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
@@ -10,7 +10,7 @@ const {
10
10
  isExcludedWalkPath,
11
11
  } = require('./scan-exclusions');
12
12
 
13
- // Monorepo support (skill-code-certification, issue 008): manifests are discovered RECURSIVELY under `root`, not just at the top level.
13
+ // Monorepo support: manifests are discovered RECURSIVELY under `root`, not just at the top level.
14
14
 
15
15
  // Exact dependency-name -> canonical framework/library name (npm + pip).
16
16
  const EXACT_FRAMEWORK_MAP = {
@@ -202,7 +202,7 @@ function fromPyprojectTomlFile(filePath) {
202
202
  return names;
203
203
  }
204
204
 
205
- // Manifest filename -> reader (skill-code-certification, issue 008).
205
+ // Manifest filename -> reader.
206
206
  const MANIFEST_READERS = {
207
207
  'package.json': fromPackageJsonFile,
208
208
  'requirements.txt': fromRequirementsTxtFile,
@@ -146,7 +146,7 @@ header.top{
146
146
  .col-head{display:flex; align-items:baseline; gap:10px; margin:2px 0 14px}
147
147
  .col-head h2{margin:0; font-size:13px; letter-spacing:.12em; text-transform:uppercase; color:var(--fg-muted); font-weight:650}
148
148
  .col-head .dot{width:6px;height:6px;border-radius:50%; background:var(--accent)}
149
- /* Column note (issue 096): one line under the column head, for copy that has to
149
+ /* Column note: one line under the column head, for copy that has to
150
150
  be read before the numbers below it. `.sub` only exists inside `.card`. */
151
151
  .col-note{margin:-8px 0 14px; font-size:12.5px; line-height:1.5; color:var(--fg-muted)}
152
152
  .col-note b{color:var(--fg); font-weight:650}
@@ -211,16 +211,15 @@ header.top{
211
211
  .chip .d{width:7px;height:7px;border-radius:50%; background:var(--accent)}
212
212
 
213
213
  /* --- Agent tree --------------------------------------------- *
214
- * ORPHANED BUT OWNED (issue 090, kept for issue 089).
214
+ * ORPHANED BUT OWNED.
215
215
  *
216
216
  * Nothing emits `class="agent"` today: `render-sheet.js` builds no agent tree, so
217
217
  * this block styles markup that does not exist. It is NOT dead by accident — it
218
- * came with the approved mockup and it is exactly the material issue 089 (the
219
- * shareable report has no agents at all) needs, so that design does not have to
220
- * re-invent the nesting rail. Recovering the markup that used to pair with it:
221
- * `git show 7fae2c8^:src/render-html.js` (see `agentNodeHtml`/`agentCardsSection`).
218
+ * came with the approved mockup and it is exactly the material an agent section in
219
+ * the shareable report (which has no agents at all) needs, so that design does not
220
+ * have to re-invent the nesting rail.
222
221
  *
223
- * If 089 is dropped or solved another way, this goes with it. What it must not
222
+ * If that section is dropped or solved another way, this goes with it. What it must not
224
223
  * do is sit here with no owner, which is how it got orphaned in the first place.
225
224
  * ------------------------------------------------------------ */
226
225
  .tree{margin:0; padding:0; list-style:none}
@@ -312,7 +311,7 @@ header.top{
312
311
  .tag-na{background:var(--chip-bg); color:var(--fg-muted)}
313
312
  .area .icn{width:16px; text-align:center}
314
313
 
315
- /* anchored quotes (issue 054): one block per quote, under its own area row, with
314
+ /* anchored quotes: one block per quote, under its own area row, with
316
315
  a left rail whose colour differs per SOURCE. The .qt-k label is the real
317
316
  distinction between the two — the rail is lost in greyscale, the label is not. */
318
317
  .area-block{display:flex; flex-direction:column; gap:3px}
@@ -401,7 +400,7 @@ __BODY__
401
400
  "use strict";
402
401
 
403
402
  /* Runs ONE independent init/update step in isolation from its siblings
404
- (issue 067). `load`, `resize` and the theme toggle each chain several
403
+ `load`, `resize` and the theme toggle each chain several
405
404
  UNRELATED effects (paint the ring, count up the score, slide the tab
406
405
  indicator, sync a label…) as a plain sequence of statements. Before this,
407
406
  one throwing — as `paintRing()` did pre-066, on ITS OWN unguarded lookup —
@@ -448,9 +447,8 @@ __BODY__
448
447
  reached countUp / moveInd / syncExpandLabel — leaving the tab indicator
449
448
  unpositioned and the expand-all label unsynced — and the theme toggle, whose
450
449
  handler ends in paintRing(), threw on every single click. A broken script
451
- fails no test; it silently degrades a report someone is reading. Found in
452
- issue 066, when a report with only an agent certification became reachable
453
- for the first time. */
450
+ fails no test; it silently degrades a report someone is reading. Found when
451
+ a report with only an agent certification first became reachable. */
454
452
  if(!fill) return;
455
453
  var circumference = 2 * Math.PI * 54; // r=54
456
454
  fill.style.strokeDasharray = circumference;
@@ -556,7 +554,7 @@ __BODY__
556
554
  });
557
555
 
558
556
  /* ---- init ---- */
559
- // Four UNRELATED effects (issue 067): each isolated so one failing never
557
+ // Four UNRELATED effects: each isolated so one failing never
560
558
  // takes the other three down with it — see safeStep() above for why this,
561
559
  // and not a guard on #ringFill/#scoreNum alone, is the fix at this level.
562
560
  window.addEventListener('load', function(){