@jaimevalasek/aioson 1.21.3 → 1.21.4

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 (134) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/package.json +1 -1
  3. package/src/agents.js +23 -22
  4. package/src/cli.js +43 -20
  5. package/src/commands/agent-audit.js +189 -119
  6. package/src/commands/artifact-validate.js +31 -14
  7. package/src/commands/context-health.js +170 -34
  8. package/src/commands/devlog-process.js +35 -13
  9. package/src/commands/learning.js +98 -19
  10. package/src/commands/live.js +48 -22
  11. package/src/commands/preflight.js +16 -7
  12. package/src/commands/quality-audit.js +119 -0
  13. package/src/commands/skill-audit.js +200 -0
  14. package/src/commands/squad-playbook.js +100 -0
  15. package/src/commands/squad-role-scan.js +188 -0
  16. package/src/commands/state-save.js +9 -7
  17. package/src/commands/workflow-execute.js +172 -32
  18. package/src/commands/workflow-next.js +148 -40
  19. package/src/commands/workflow-status.js +54 -22
  20. package/src/handoff-contract.js +11 -6
  21. package/src/i18n/messages/en.js +13 -7
  22. package/src/i18n/messages/es.js +7 -5
  23. package/src/i18n/messages/fr.js +7 -5
  24. package/src/i18n/messages/pt-BR.js +13 -7
  25. package/src/learning-import-claude.js +218 -0
  26. package/src/learning-loop-engine.js +268 -254
  27. package/src/learning-loop-migration.js +177 -163
  28. package/src/learning-materialize.js +192 -0
  29. package/src/lib/quality/provider.js +132 -0
  30. package/src/lib/quality/report.js +82 -0
  31. package/src/lib/quality/result.js +185 -0
  32. package/src/parser.js +5 -4
  33. package/src/preflight-engine.js +49 -22
  34. package/src/runtime-store.js +2 -1
  35. package/template/.aioson/agents/analyst.md +18 -6
  36. package/template/.aioson/agents/committer.md +5 -5
  37. package/template/.aioson/agents/copywriter.md +27 -27
  38. package/template/.aioson/agents/dev.md +58 -39
  39. package/template/.aioson/agents/deyvin.md +43 -32
  40. package/template/.aioson/agents/discovery-design-doc.md +27 -13
  41. package/template/.aioson/agents/genome.md +81 -82
  42. package/template/.aioson/agents/manifests/dev.manifest.json +5 -4
  43. package/template/.aioson/agents/manifests/deyvin.manifest.json +4 -3
  44. package/template/.aioson/agents/neo.md +1 -1
  45. package/template/.aioson/agents/orchestrator.md +1 -1
  46. package/template/.aioson/agents/pentester.md +2 -2
  47. package/template/.aioson/agents/product.md +27 -19
  48. package/template/.aioson/agents/qa.md +4 -4
  49. package/template/.aioson/agents/setup.md +1 -1
  50. package/template/.aioson/agents/site-forge.md +17 -19
  51. package/template/.aioson/agents/squad.md +4 -0
  52. package/template/.aioson/agents/tester.md +178 -153
  53. package/template/.aioson/agents/ux-ui.md +1 -1
  54. package/template/.aioson/config.md +12 -12
  55. package/template/.aioson/context/design-doc.md +136 -136
  56. package/template/.aioson/context/project-map.md +7 -5
  57. package/template/.aioson/context/seeds/seed-example.md +27 -27
  58. package/template/.aioson/context/user-profile.md +42 -42
  59. package/template/.aioson/design-docs/agent-loading-contract.md +117 -138
  60. package/template/.aioson/docs/dev/simple-plan-lane.md +92 -0
  61. package/template/.aioson/docs/product/conversation-playbook.md +15 -17
  62. package/template/.aioson/docs/site-forge-build.md +2 -2
  63. package/template/.aioson/docs/site-forge-recon.md +5 -5
  64. package/template/.aioson/docs/squad/creation-flow.md +55 -0
  65. package/template/.aioson/docs/squad/eval-gate.md +79 -0
  66. package/template/.aioson/docs/squad/package-contract.md +39 -6
  67. package/template/.aioson/docs/squad/persona-grounding.md +62 -0
  68. package/template/.aioson/docs/squad/quality-lens.md +12 -1
  69. package/template/.aioson/genomes/INDEX.md +37 -37
  70. package/template/.aioson/genomes/copywriting/references/application-notes.md +2 -2
  71. package/template/.aioson/genomes/copywriting/references/frameworks/pms-research.md +1 -1
  72. package/template/.aioson/genomes/copywriting-brunson/references/application-notes.md +2 -2
  73. package/template/.aioson/learnings/gotchas/.gitkeep +1 -0
  74. package/template/.aioson/learnings/recipes/.gitkeep +1 -0
  75. package/template/.aioson/rules/agent-language-policy.md +21 -21
  76. package/template/.aioson/rules/agent-structural-contract.md +2 -2
  77. package/template/.aioson/rules/aioson-context-boundary.md +8 -6
  78. package/template/.aioson/rules/canonical-path-contract.md +10 -5
  79. package/template/.aioson/rules/data-format-convention.md +11 -11
  80. package/template/.aioson/rules/disk-first-artifacts.md +5 -4
  81. package/template/.aioson/rules/prd-section-ownership.md +12 -12
  82. package/template/.aioson/rules/simple-plan-lane.md +48 -0
  83. package/template/.aioson/rules/spec-level-ownership.md +5 -4
  84. package/template/.aioson/schemas/squad-blueprint.schema.json +32 -11
  85. package/template/.aioson/schemas/squad-manifest.schema.json +29 -8
  86. package/template/.aioson/skills/design/clean-saas-ui/SKILL.md +4 -4
  87. package/template/.aioson/skills/design/clean-saas-ui/references/art-direction.md +30 -30
  88. package/template/.aioson/skills/design/clean-saas-ui/references/motion.md +4 -4
  89. package/template/.aioson/skills/design/cognitive-core-ui/SKILL.md +2 -2
  90. package/template/.aioson/skills/design/cognitive-core-ui/references/design-tokens.md +1 -1
  91. package/template/.aioson/skills/design/cognitive-core-ui/references/patterns.md +1 -1
  92. package/template/.aioson/skills/design/neo-brutalist-ui/SKILL.md +5 -5
  93. package/template/.aioson/skills/design/pt.squarespace.com/references/components.md +2 -2
  94. package/template/.aioson/skills/design/pt.squarespace.com/references/websites.md +4 -4
  95. package/template/.aioson/skills/design-system/dashboards/SKILL.md +5 -5
  96. package/template/.aioson/skills/design-system/patterns/SKILL.md +1 -1
  97. package/template/.aioson/skills/marketing/references/cta-matrix.md +43 -43
  98. package/template/.aioson/skills/marketing/references/headline-matrix.md +33 -33
  99. package/template/.aioson/skills/marketing/references/market-intelligence.md +2 -2
  100. package/template/.aioson/skills/marketing/references/platform-constraints.md +2 -2
  101. package/template/.aioson/skills/marketing/references/pms-research.md +3 -3
  102. package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +7 -7
  103. package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +13 -11
  104. package/template/.aioson/skills/process/aioson-spec-driven/references/ui-language.md +85 -75
  105. package/template/.aioson/skills/process/decision-presentation/SKILL.md +11 -11
  106. package/template/.aioson/skills/process/decision-presentation/references/jargon-map.pt-BR.yaml +4 -4
  107. package/template/.aioson/skills/squad/references/executor-archetypes.md +77 -2
  108. package/template/.aioson/skills/static/harness-validate/SKILL.md +55 -46
  109. package/template/.aioson/skills/static/react-motion-patterns.md +1 -1
  110. package/template/.aioson/skills/static/static-html-patterns.md +2 -2
  111. package/template/.aioson/skills/static/threejs-patterns.md +2 -2
  112. package/template/.aioson/tasks/implementation-plan.md +325 -327
  113. package/template/.aioson/tasks/squad-analyze.md +93 -83
  114. package/template/.aioson/tasks/squad-create.md +156 -148
  115. package/template/.aioson/tasks/squad-design.md +223 -206
  116. package/template/.aioson/tasks/squad-eval.md +72 -0
  117. package/template/.aioson/tasks/squad-execution-plan.md +279 -279
  118. package/template/.aioson/tasks/squad-export.md +20 -20
  119. package/template/.aioson/tasks/squad-extend.md +73 -68
  120. package/template/.aioson/tasks/squad-investigate.md +57 -57
  121. package/template/.aioson/tasks/squad-pipeline.md +122 -122
  122. package/template/.aioson/tasks/squad-profile.md +48 -48
  123. package/template/.aioson/tasks/squad-refresh.md +242 -236
  124. package/template/.aioson/tasks/squad-repair.md +85 -85
  125. package/template/.aioson/tasks/squad-review.md +61 -61
  126. package/template/.aioson/tasks/squad-task-decompose.md +66 -66
  127. package/template/.aioson/tasks/squad-validate.md +65 -58
  128. package/template/.aioson/templates/squads/content-basic/template.json +1 -1
  129. package/template/.aioson/templates/squads/media-channel/template.json +1 -1
  130. package/template/.aioson/templates/squads/research-analysis/template.json +1 -1
  131. package/template/AGENTS.md +10 -6
  132. package/template/CLAUDE.md +10 -6
  133. package/template/OPENCODE.md +9 -5
  134. package/template/agents/_shared/learning-capture-directive.md +88 -0
@@ -21,21 +21,145 @@ function formatTokens(n) {
21
21
  return `~${n.toLocaleString()}`;
22
22
  }
23
23
 
24
- async function loadFeatureStatuses(contextDir) {
25
- const featuresPath = path.join(contextDir, 'features.md');
26
- try {
27
- const content = await fs.readFile(featuresPath, 'utf8');
28
- const done = new Set();
29
- for (const line of content.split(/\r?\n/)) {
30
- // Match lines like: - auth: done or | auth | done |
31
- const m = line.match(/[-|]\s*([a-z0-9_-]+)\s*[:|]\s*done/i);
32
- if (m) done.add(m[1].toLowerCase());
33
- }
34
- return done;
35
- } catch {
36
- return new Set();
37
- }
38
- }
24
+ async function loadFeatureStatuses(contextDir) {
25
+ const registry = await loadFeatureRegistry(contextDir);
26
+ return new Set(registry.done.map((feature) => feature.slug.toLowerCase()));
27
+ }
28
+
29
+ async function loadFeatureRegistry(contextDir) {
30
+ const featuresPath = path.join(contextDir, 'features.md');
31
+ try {
32
+ const content = await fs.readFile(featuresPath, 'utf8');
33
+ const features = [];
34
+ for (const line of content.split(/\r?\n/)) {
35
+ const table = line.match(/^\|\s*([a-z0-9_-]+)\s*\|\s*([a-z_ -]+)\s*\|/i);
36
+ if (table && table[1] !== 'slug') {
37
+ features.push({ slug: table[1].trim(), status: table[2].trim().toLowerCase() });
38
+ continue;
39
+ }
40
+
41
+ const list = line.match(/^-\s*([a-z0-9_-]+)\s*:\s*([a-z_ -]+)/i);
42
+ if (list) {
43
+ features.push({ slug: list[1].trim(), status: list[2].trim().toLowerCase() });
44
+ }
45
+ }
46
+ return {
47
+ all: features,
48
+ active: features.filter((feature) => feature.status === 'in_progress'),
49
+ done: features.filter((feature) => feature.status === 'done')
50
+ };
51
+ } catch {
52
+ return { all: [], active: [], done: [] };
53
+ }
54
+ }
55
+
56
+ function parseFrontmatter(content) {
57
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
58
+ if (!match) return {};
59
+
60
+ const result = {};
61
+ for (const line of match[1].split(/\r?\n/)) {
62
+ const idx = line.indexOf(':');
63
+ if (idx === -1) continue;
64
+ const key = line.slice(0, idx).trim();
65
+ let value = line.slice(idx + 1).trim();
66
+ value = value.replace(/^["']|["']$/g, '');
67
+ result[key] = value;
68
+ }
69
+ return result;
70
+ }
71
+
72
+ async function readProjectClassification(contextDir) {
73
+ try {
74
+ const content = await fs.readFile(path.join(contextDir, 'project.context.md'), 'utf8');
75
+ return parseFrontmatter(content).classification || null;
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ async function readWorkflowState(contextDir) {
82
+ try {
83
+ const raw = await fs.readFile(path.join(contextDir, 'workflow.state.json'), 'utf8');
84
+ return JSON.parse(raw);
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ async function readPulseActiveFeature(contextDir) {
91
+ try {
92
+ const content = await fs.readFile(path.join(contextDir, 'project-pulse.md'), 'utf8');
93
+ const frontmatter = parseFrontmatter(content);
94
+ return normalizePulseFeature(frontmatter.active_feature || null);
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ function normalizePulseFeature(value) {
101
+ if (!value) return null;
102
+ const normalized = String(value).trim().replace(/^["']|["']$/g, '');
103
+ if (!normalized || ['none', 'project', '(none)', '-', '—'].includes(normalized.toLowerCase())) return null;
104
+ return normalized;
105
+ }
106
+
107
+ async function buildDriftWarnings(contextDir) {
108
+ const warnings = [];
109
+ const projectClassification = await readProjectClassification(contextDir);
110
+ const workflowState = await readWorkflowState(contextDir);
111
+ const featureRegistry = await loadFeatureRegistry(contextDir);
112
+ const pulseActiveFeature = await readPulseActiveFeature(contextDir);
113
+
114
+ if (
115
+ projectClassification &&
116
+ workflowState?.mode === 'feature' &&
117
+ workflowState.classification &&
118
+ projectClassification.toUpperCase() !== String(workflowState.classification).toUpperCase()
119
+ ) {
120
+ warnings.push({
121
+ id: 'classification_drift',
122
+ severity: 'warning',
123
+ message: `Project classification is ${projectClassification}; active workflow feature classification is ${workflowState.classification}.`,
124
+ suggested_command: 'aioson context:health . --json'
125
+ });
126
+ }
127
+
128
+ if (featureRegistry.active.length > 1) {
129
+ warnings.push({
130
+ id: 'multiple_active_features',
131
+ severity: 'warning',
132
+ message: `features.md has multiple in_progress features: ${featureRegistry.active.map((feature) => feature.slug).join(', ')}.`,
133
+ suggested_command: 'aioson feature:sweep . --dry-run'
134
+ });
135
+ }
136
+
137
+ const activeFeature = featureRegistry.active[0]?.slug || null;
138
+ if (activeFeature && pulseActiveFeature && activeFeature !== pulseActiveFeature) {
139
+ warnings.push({
140
+ id: 'active_state_drift',
141
+ severity: 'warning',
142
+ message: `features.md active feature is ${activeFeature}; project-pulse.md active_feature is ${pulseActiveFeature}.`,
143
+ suggested_command: 'aioson pulse:update . --feature=' + activeFeature
144
+ });
145
+ } else if (activeFeature && pulseActiveFeature === null) {
146
+ warnings.push({
147
+ id: 'active_state_drift',
148
+ severity: 'warning',
149
+ message: `features.md active feature is ${activeFeature}; project-pulse.md has no active_feature.`,
150
+ suggested_command: 'aioson pulse:update . --feature=' + activeFeature
151
+ });
152
+ } else if (!activeFeature && pulseActiveFeature) {
153
+ warnings.push({
154
+ id: 'active_state_drift',
155
+ severity: 'warning',
156
+ message: `project-pulse.md active_feature is ${pulseActiveFeature}, but features.md has no in_progress feature with that slug.`,
157
+ suggested_command: 'aioson pulse:update .'
158
+ });
159
+ }
160
+
161
+ return warnings;
162
+ }
39
163
 
40
164
  async function getCacheHitRate(db) {
41
165
  if (!db) return null;
@@ -114,7 +238,7 @@ async function runContextHealth({ args, options = {}, logger }) {
114
238
 
115
239
  report.sort((a, b) => b.tokens - a.tokens);
116
240
 
117
- const doneFeatures = await loadFeatureStatuses(contextDir);
241
+ const doneFeatures = await loadFeatureStatuses(contextDir);
118
242
  const staleSpecs = report.filter((r) => {
119
243
  if (!r.file.startsWith('spec-')) return false;
120
244
  const slug = r.file.replace(/^spec-/, '').replace(/\.md$/, '');
@@ -137,17 +261,19 @@ async function runContextHealth({ args, options = {}, logger }) {
137
261
  db.close();
138
262
  }
139
263
 
140
- const skeletonPresent = entries.includes('skeleton-system.md') || entries.includes('skeleton.md');
141
-
142
- if (options.json) {
143
- return {
144
- ok: true,
145
- totalTokens,
146
- files: report,
147
- staleSpecs: staleSpecs.map((s) => s.file),
148
- cacheHitRate,
149
- skeletonPresent,
150
- dbPath
264
+ const skeletonPresent = entries.includes('skeleton-system.md') || entries.includes('skeleton.md');
265
+ const driftWarnings = await buildDriftWarnings(contextDir);
266
+
267
+ if (options.json) {
268
+ return {
269
+ ok: true,
270
+ totalTokens,
271
+ files: report,
272
+ staleSpecs: staleSpecs.map((s) => s.file),
273
+ driftWarnings,
274
+ cacheHitRate,
275
+ skeletonPresent,
276
+ dbPath
151
277
  };
152
278
  }
153
279
 
@@ -189,7 +315,7 @@ async function runContextHealth({ args, options = {}, logger }) {
189
315
  logger.log('');
190
316
  }
191
317
 
192
- if (staleSpecs.length > 0) {
318
+ if (staleSpecs.length > 0) {
193
319
  logger.log(`⚠ ${staleSpecs.length} stale spec file(s) (features: done):`);
194
320
  for (const s of staleSpecs) {
195
321
  const slug = s.file.replace(/^spec-/, '').replace(/\.md$/, '');
@@ -197,7 +323,16 @@ async function runContextHealth({ args, options = {}, logger }) {
197
323
  }
198
324
  logger.log(` Run: aioson feature:archive . --feature=<slug> to archive them`);
199
325
  logger.log('');
200
- }
326
+ }
327
+
328
+ if (driftWarnings.length > 0) {
329
+ logger.log(`⚠ ${driftWarnings.length} context drift warning(s):`);
330
+ for (const warning of driftWarnings) {
331
+ logger.log(` → ${warning.message}`);
332
+ if (warning.suggested_command) logger.log(` ${warning.suggested_command}`);
333
+ }
334
+ logger.log('');
335
+ }
201
336
 
202
337
  if (cacheHitRate !== null) {
203
338
  logger.log(`✓ Cache hit rate: ${cacheHitRate}% (last 7 days)`);
@@ -208,11 +343,12 @@ async function runContextHealth({ args, options = {}, logger }) {
208
343
 
209
344
  return {
210
345
  ok: true,
211
- totalTokens,
212
- files: report,
213
- staleSpecs: staleSpecs.map((s) => s.file),
214
- cacheHitRate,
215
- skeletonPresent,
346
+ totalTokens,
347
+ files: report,
348
+ staleSpecs: staleSpecs.map((s) => s.file),
349
+ driftWarnings,
350
+ cacheHitRate,
351
+ skeletonPresent,
216
352
  dbPath
217
353
  };
218
354
  }
@@ -50,11 +50,20 @@ function extractTaggedLearnings(content) {
50
50
  for (const line of section.split(/\r?\n/)) {
51
51
  const trimmed = line.replace(/^[-*]\s*/, '').trim();
52
52
  if (!trimmed) continue;
53
- const typeMatch = trimmed.match(/^\[(process|domain|quality|preference)\]\s+(.+)/i);
53
+ const typeMatch = trimmed.match(/^\[(process|domain|quality|preference|gotcha|resolution)\]\s+(.+)/i);
54
54
  if (typeMatch) {
55
- learnings.push({ type: typeMatch[1].toLowerCase(), title: typeMatch[2].trim() });
55
+ const tag = typeMatch[1].toLowerCase();
56
+ const title = typeMatch[2].trim();
57
+ // cross-tool-project-knowledge: gotcha/resolution are project-knowledge
58
+ // signals — persisted under type='quality' with the real signal in `kind`
59
+ // (project_learnings.type CHECK only allows the 4 base types).
60
+ if (tag === 'gotcha' || tag === 'resolution') {
61
+ learnings.push({ type: 'quality', kind: tag, title });
62
+ } else {
63
+ learnings.push({ type: tag, kind: null, title });
64
+ }
56
65
  } else if (trimmed.length > 5) {
57
- learnings.push({ type: 'process', title: trimmed });
66
+ learnings.push({ type: 'process', kind: null, title: trimmed });
58
67
  }
59
68
  }
60
69
  return learnings;
@@ -69,25 +78,38 @@ function extractSummary(content) {
69
78
  return firstHeading ? firstHeading[1].trim() : null;
70
79
  }
71
80
 
72
- function upsertProjectLearning(db, { title, type, featureSlug, evidence, sourceSession }) {
81
+ // cross-tool-project-knowledge: app-level allow-list for project_learnings.kind.
82
+ // The column carries no schema CHECK by repo convention (see
83
+ // learning-loop-migration.js Phase 4). NULL = not a project-knowledge learning.
84
+ const ALLOWED_LEARNING_KINDS = new Set(['gotcha', 'resolution']);
85
+
86
+ function normalizeKind(kind) {
87
+ return ALLOWED_LEARNING_KINDS.has(kind) ? kind : null;
88
+ }
89
+
90
+ function upsertProjectLearning(db, { title, type, kind, featureSlug, evidence, sourceSession }) {
91
+ const safeKind = normalizeKind(kind);
73
92
  const existing = db.prepare(
74
- 'SELECT learning_id, frequency FROM project_learnings WHERE title = ? AND (feature_slug = ? OR (feature_slug IS NULL AND ? IS NULL))'
93
+ 'SELECT learning_id, frequency, kind FROM project_learnings WHERE title = ? AND (feature_slug = ? OR (feature_slug IS NULL AND ? IS NULL))'
75
94
  ).get(title, featureSlug || null, featureSlug || null);
76
95
 
77
96
  if (existing) {
97
+ // Enrich kind only when previously unset — a plain re-tag must not clobber
98
+ // an existing classification.
99
+ const nextKind = existing.kind || safeKind || null;
78
100
  db.prepare(
79
- 'UPDATE project_learnings SET frequency = ?, last_reinforced = ?, updated_at = ? WHERE learning_id = ?'
80
- ).run(existing.frequency + 1, nowIso(), nowIso(), existing.learning_id);
101
+ 'UPDATE project_learnings SET frequency = ?, last_reinforced = ?, updated_at = ?, kind = ? WHERE learning_id = ?'
102
+ ).run(existing.frequency + 1, nowIso(), nowIso(), nextKind, existing.learning_id);
81
103
  return { action: 'updated', learningId: existing.learning_id };
82
104
  }
83
105
 
84
106
  const learningId = createLearningId();
85
107
  db.prepare(`
86
108
  INSERT INTO project_learnings
87
- (learning_id, feature_slug, type, title, confidence, frequency, last_reinforced,
109
+ (learning_id, feature_slug, type, kind, title, confidence, frequency, last_reinforced,
88
110
  applies_to, source_session, evidence, status, created_at, updated_at)
89
- VALUES (?, ?, ?, ?, 'medium', 1, ?, 'project', ?, ?, 'active', ?, ?)
90
- `).run(learningId, featureSlug || null, type, title, nowIso(), sourceSession || null, evidence || null, nowIso(), nowIso());
111
+ VALUES (?, ?, ?, ?, ?, 'medium', 1, ?, 'project', ?, ?, 'active', ?, ?)
112
+ `).run(learningId, featureSlug || null, type, safeKind, title, nowIso(), sourceSession || null, evidence || null, nowIso(), nowIso());
91
113
  return { action: 'inserted', learningId };
92
114
  }
93
115
 
@@ -168,8 +190,8 @@ async function processDevlogFile(db, filePath) {
168
190
 
169
191
  // Upsert learnings
170
192
  const learnings = extractTaggedLearnings(body);
171
- for (const { type, title } of learnings) {
172
- upsertProjectLearning(db, { title, type, featureSlug, sourceSession: sessionKey || path.basename(filePath) });
193
+ for (const { type, title, kind } of learnings) {
194
+ upsertProjectLearning(db, { title, type, kind, featureSlug, sourceSession: sessionKey || path.basename(filePath) });
173
195
  }
174
196
 
175
197
  // Log verdict if present
@@ -291,4 +313,4 @@ async function runDevlogProcess({ args, options = {}, logger }) {
291
313
  return { ok: true, results, processed: processed.length, skipped: skipped.length, malformed: malformed.length, totalArtifacts, totalLearnings, dbPath };
292
314
  }
293
315
 
294
- module.exports = { runDevlogProcess, processDevlogFile };
316
+ module.exports = { runDevlogProcess, processDevlogFile, extractTaggedLearnings, upsertProjectLearning };
@@ -1,13 +1,19 @@
1
1
  'use strict';
2
2
 
3
- const path = require('node:path');
4
- const {
5
- openRuntimeDb,
6
- listProjectLearnings,
3
+ const path = require('node:path');
4
+ const {
5
+ openRuntimeDb,
6
+ listProjectLearnings,
7
7
  getProjectLearning,
8
- promoteProjectLearning,
9
- getProjectLearningStats
10
- } = require('../runtime-store');
8
+ promoteProjectLearning,
9
+ getProjectLearningStats
10
+ } = require('../runtime-store');
11
+ const {
12
+ loadClaudeMemoryCandidates,
13
+ parseSelection,
14
+ isSelected
15
+ } = require('../learning-import-claude');
16
+ const { upsertProjectLearning } = require('./devlog-process');
11
17
 
12
18
  /**
13
19
  * Subcommand: list [--status=active|stale|archived|promoted]
@@ -78,7 +84,7 @@ async function handleStats(projectDir, { logger, t }) {
78
84
  * Subcommand: promote <learning-id> --to=<rule-path>
79
85
  * Promotes a learning to a project rule.
80
86
  */
81
- async function handlePromote(projectDir, learningId, promotedTo, { logger, t }) {
87
+ async function handlePromote(projectDir, learningId, promotedTo, { logger, t }) {
82
88
  if (!learningId) {
83
89
  logger.error(t('learning.promote_usage'));
84
90
  return { promoted: false };
@@ -106,7 +112,77 @@ async function handlePromote(projectDir, learningId, promotedTo, { logger, t })
106
112
  } finally {
107
113
  db.close();
108
114
  }
109
- }
115
+ }
116
+
117
+ /**
118
+ * Subcommand: import-from-claude [--project-hash=<hash>] [--dry-run] [--select=1,2|all]
119
+ * Imports technical Claude Code project memory into project_learnings.
120
+ */
121
+ async function handleImportFromClaude(projectDir, options, { logger }) {
122
+ let loaded;
123
+ try {
124
+ loaded = await loadClaudeMemoryCandidates({
125
+ targetDir: projectDir,
126
+ projectHash: options['project-hash'] || options.projectHash,
127
+ claudeHome: options['claude-home'] || options.claudeHome
128
+ });
129
+ } catch (err) {
130
+ logger.error(err.message);
131
+ return { ok: false, error: err.code || 'import_failed', candidates: [], promoted: 0 };
132
+ }
133
+
134
+ const selection = parseSelection(options.select);
135
+ const dryRun = Boolean(options['dry-run'] || options.dryRun);
136
+ const candidates = loaded.candidates;
137
+
138
+ logger.log(`Claude memory candidates (${candidates.length}) — ${loaded.hash}`);
139
+ for (const candidate of candidates) {
140
+ const marker = candidate.kind ? candidate.kind : candidate.classification;
141
+ logger.log(` [${candidate.index}] ${marker}: ${candidate.title} (${candidate.source})`);
142
+ }
143
+
144
+ if (dryRun || !selection) {
145
+ if (!selection) logger.log('Run again with --select=<n[,n]|all> to import technical candidates.');
146
+ return {
147
+ ok: true,
148
+ dryRun: true,
149
+ requiresSelection: !selection,
150
+ projectHash: loaded.hash,
151
+ candidates,
152
+ promoted: 0,
153
+ skipped: 0
154
+ };
155
+ }
156
+
157
+ const handle = await openRuntimeDb(projectDir);
158
+ const { db } = handle;
159
+ const promoted = [];
160
+ const skipped = [];
161
+ try {
162
+ for (const candidate of candidates) {
163
+ if (!isSelected(selection, candidate.index)) continue;
164
+ if (!candidate.kind) {
165
+ skipped.push({ index: candidate.index, title: candidate.title, reason: candidate.classification });
166
+ continue;
167
+ }
168
+ const result = upsertProjectLearning(db, {
169
+ title: candidate.title,
170
+ type: 'quality',
171
+ kind: candidate.kind,
172
+ featureSlug: options.feature || null,
173
+ evidence: candidate.evidence,
174
+ sourceSession: `claude-memory:${loaded.hash}:${candidate.source}`
175
+ });
176
+ promoted.push({ ...result, index: candidate.index, title: candidate.title, kind: candidate.kind });
177
+ }
178
+ } finally {
179
+ db.close();
180
+ }
181
+
182
+ logger.log(`Imported: ${promoted.length}`);
183
+ if (skipped.length > 0) logger.log(`Skipped: ${skipped.length}`);
184
+ return { ok: true, dryRun: false, projectHash: loaded.hash, candidates, promoted, skipped };
185
+ }
110
186
 
111
187
  /**
112
188
  * Entry point for CLI integration.
@@ -122,13 +198,16 @@ async function runLearning({ args = [], options = {}, logger = console, t = (k)
122
198
  if (sub === 'stats') {
123
199
  return handleStats(projectDir, context);
124
200
  }
125
- if (sub === 'promote') {
126
- const learningId = args[2] || options.id;
127
- return handlePromote(projectDir, learningId, options.to || null, context);
128
- }
129
-
130
- logger.error(`Unknown subcommand: ${sub}. Available: list, stats, promote`);
131
- return { error: true };
132
- }
133
-
134
- module.exports = { runLearning, handleList, handleStats, handlePromote };
201
+ if (sub === 'promote') {
202
+ const learningId = args[2] || options.id;
203
+ return handlePromote(projectDir, learningId, options.to || null, context);
204
+ }
205
+ if (sub === 'import-from-claude') {
206
+ return handleImportFromClaude(projectDir, options, context);
207
+ }
208
+
209
+ logger.error(`Unknown subcommand: ${sub}. Available: list, stats, promote, import-from-claude`);
210
+ return { error: true };
211
+ }
212
+
213
+ module.exports = { runLearning, handleList, handleStats, handlePromote, handleImportFromClaude };
@@ -19,7 +19,7 @@ const {
19
19
  const { ensureDir, exists } = require('../utils');
20
20
  const { SUPPORTED_PROMPT_TOOLS } = require('../prompt-tool');
21
21
  const { isTmuxAvailable, launchTmuxSession, buildSessionName, hasSession, attachSession } = require('../lib/tmux-launcher');
22
- const { resolvePermissionModeArgs, resolveResumeArgs } = require('../lib/tool-capabilities');
22
+ const { resolvePermissionModeArgs, resolveResumeArgs } = require('../lib/tool-capabilities');
23
23
 
24
24
  const LIVE_EVENTS_LIMIT = 10;
25
25
  const LIVE_MESSAGE_LIMIT = 500;
@@ -116,16 +116,16 @@ function parseJsonOption(value) {
116
116
  }
117
117
  }
118
118
 
119
- // Combine `--resume` (mapped per-tool via TOOL_CAPS) with user-provided `--tool-args`.
120
- // Resume args go FIRST so that codex `resume --last` (subcommand) lands at argv[1].
121
- function buildLaunchArgs(options, tool) {
122
- const resumeOpt = options.resume !== undefined ? options.resume : options.Resume;
123
- const resumeArgs = resolveResumeArgs(tool, resumeOpt);
124
- const permissionMode = options['permission-mode'] || options.permissionMode;
125
- const permissionArgs = resolvePermissionModeArgs(tool, permissionMode);
126
- const userArgs = parseToolArgs(options['tool-args'] || options.toolArgs);
127
- return [...resumeArgs, ...permissionArgs, ...userArgs];
128
- }
119
+ // Combine `--resume` (mapped per-tool via TOOL_CAPS) with user-provided `--tool-args`.
120
+ // Resume args go FIRST so that codex `resume --last` (subcommand) lands at argv[1].
121
+ function buildLaunchArgs(options, tool) {
122
+ const resumeOpt = options.resume !== undefined ? options.resume : options.Resume;
123
+ const resumeArgs = resolveResumeArgs(tool, resumeOpt);
124
+ const permissionMode = options['permission-mode'] || options.permissionMode;
125
+ const permissionArgs = resolvePermissionModeArgs(tool, permissionMode);
126
+ const userArgs = parseToolArgs(options['tool-args'] || options.toolArgs);
127
+ return [...resumeArgs, ...permissionArgs, ...userArgs];
128
+ }
129
129
 
130
130
  function parseToolArgs(value) {
131
131
  if (value === undefined || value === null || value === '') return [];
@@ -253,6 +253,15 @@ async function resolveExecutablePath(command) {
253
253
  return null;
254
254
  }
255
255
 
256
+ // Com `shell: true` (Windows), o comando vai pro cmd.exe. Um caminho com espaços
257
+ // — ex.: "C:\Program Files\nodejs\codex.cmd" — quebra se não for quotado (o
258
+ // cmd.exe corta no primeiro espaço e tenta rodar "C:\Program"). Quotamos o
259
+ // executável; sem shell (Unix) ele vai cru. Resolve o ENOENT/falha ao iniciar
260
+ // codex/claude no Windows quando o npm bin fica no Program Files.
261
+ function spawnExecutable(binaryPath) {
262
+ return process.platform === 'win32' ? `"${binaryPath}"` : binaryPath;
263
+ }
264
+
256
265
  function detectProcessState(pid) {
257
266
  if (!pid) return 'not_tracked';
258
267
  try {
@@ -1135,7 +1144,12 @@ async function getLiveStatusSnapshot(targetDir, t, options = {}) {
1135
1144
 
1136
1145
  async function runLiveStart({ args, options = {}, logger, t }) {
1137
1146
  const targetDir = resolveTargetDir(args);
1138
- const agentName = normalizeAgentHandle(requireOption(options, 'agent', t));
1147
+ // --agent é OPCIONAL: serve só pra tagueamento/tracking da sessão (session
1148
+ // key, run, runtime emit). live:start NÃO invoca/injeta o agente — isso é
1149
+ // feito DENTRO do harness (o usuário roda /product etc. na própria CLI). Por
1150
+ // isso o caller (ex.: o Play) não precisa forçar um agente. Default 'product'
1151
+ // quando omitido, mantendo o tracking consistente sem exigir a flag.
1152
+ const agentName = normalizeAgentHandle(options.agent || 'product');
1139
1153
  const tool = normalizeLiveTool(requireOption(options, 'tool', t), t);
1140
1154
  const noLaunch = Boolean(options['no-launch']);
1141
1155
 
@@ -1239,27 +1253,39 @@ async function runLiveStart({ args, options = {}, logger, t }) {
1239
1253
  } else {
1240
1254
  // Non-tmux reuse logic
1241
1255
  const existingTool = state.tool_session || null;
1242
- if (existingTool && existingTool !== tool) {
1243
- // Auto-close stale session when tool changed (same pattern as tmux recovery above)
1256
+ // Reconcilia sessão órfã: se o processo da sessão "ativa" já morreu
1257
+ // (Play/terminal fechado sem close limpo, ou o tool crashou DEPOIS de
1258
+ // gravar o registro), NÃO reusar — senão o start novo só loga "session
1259
+ // already active", o tool nunca sobe e a órfã trava todo restart. Morto
1260
+ // é tratado igual a troca de tool: auto-close + cria sessão nova abaixo.
1261
+ const existingProcessDead = detectProcessState(state.child_pid) === 'dead';
1262
+ const toolChanged = Boolean(existingTool && existingTool !== tool);
1263
+ if (toolChanged || existingProcessDead) {
1264
+ const closeReason = toolChanged
1265
+ ? `tool changed from ${existingTool} to ${tool}`
1266
+ : 'previous process is no longer running';
1244
1267
  updateRun(db, {
1245
1268
  runKey: existing.run.run_key,
1246
1269
  status: 'completed',
1247
- summary: `Auto-closed: tool changed from ${existingTool} to ${tool}`,
1270
+ summary: `Auto-closed: ${closeReason}`,
1248
1271
  eventType: 'session_closed',
1249
1272
  phase: 'live',
1250
- message: `Tool mismatch — auto-closed previous ${existingTool} session`
1273
+ message: `Auto-closed previous session — ${closeReason}`
1251
1274
  });
1252
1275
  if (existing.task?.task_key) {
1253
1276
  updateTask(db, {
1254
1277
  taskKey: existing.task.task_key,
1255
1278
  status: 'completed',
1256
- goal: `Auto-closed after tool change to ${tool}`
1279
+ goal: `Auto-closed (${closeReason})`
1257
1280
  });
1258
1281
  }
1259
1282
  await clearAgentSession(runtimeDir, agentName);
1260
1283
  if (!options.json) {
1261
- logger.log(t('live.tool_mismatch_auto_closed', { existing: existingTool, requested: tool }) ||
1262
- `Previous session (${existingTool}) auto-closed starting new with ${tool}`);
1284
+ const msg = toolChanged
1285
+ ? (t('live.tool_mismatch_auto_closed', { existing: existingTool, requested: tool }) ||
1286
+ `Previous session (${existingTool}) auto-closed — starting new with ${tool}`)
1287
+ : `Previous ${tool} session was dead — auto-closed, starting fresh`;
1288
+ logger.log(msg);
1263
1289
  }
1264
1290
  // Fall through to create a new session below
1265
1291
  } else {
@@ -1269,7 +1295,7 @@ async function runLiveStart({ args, options = {}, logger, t }) {
1269
1295
  let attachResult = null;
1270
1296
 
1271
1297
  if (attach && !noLaunch) {
1272
- attachChild = spawn(binaryPath, buildLaunchArgs(options, tool), {
1298
+ attachChild = spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
1273
1299
  cwd: targetDir,
1274
1300
  env: process.env,
1275
1301
  stdio: 'inherit',
@@ -1378,7 +1404,7 @@ async function runLiveStart({ args, options = {}, logger, t }) {
1378
1404
  });
1379
1405
  } else {
1380
1406
  // Fallback to normal spawn if tmux not available
1381
- child = spawn(binaryPath, buildLaunchArgs(options, tool), {
1407
+ child = spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
1382
1408
  cwd: targetDir,
1383
1409
  env: process.env,
1384
1410
  stdio: 'inherit',
@@ -1391,7 +1417,7 @@ async function runLiveStart({ args, options = {}, logger, t }) {
1391
1417
  });
1392
1418
  }
1393
1419
  } else {
1394
- child = spawn(binaryPath, buildLaunchArgs(options, tool), {
1420
+ child = spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
1395
1421
  cwd: targetDir,
1396
1422
  env: process.env,
1397
1423
  stdio: 'inherit',
@@ -77,9 +77,14 @@ async function runPreflight({ args, options = {}, logger }) {
77
77
  : null;
78
78
 
79
79
  // Determine mode
80
- const mode = slug
81
- ? (artifacts.prd.exists ? 'feature' : 'continuation')
82
- : (artifacts.project_context.exists ? 'project' : 'greenfield');
80
+ const hasFeatureArtifacts = artifacts.prd.exists
81
+ || artifacts.requirements.exists
82
+ || artifacts.spec.exists
83
+ || artifacts.implementation_plan.exists
84
+ || (manifest && manifest.exists);
85
+ const mode = slug
86
+ ? (artifacts.prd.exists ? 'feature' : (hasFeatureArtifacts ? 'continuation' : 'unframed_feature'))
87
+ : (artifacts.project_context.exists ? 'project' : 'greenfield');
83
88
 
84
89
  // Spec version + checkpoint
85
90
  const specVersion = extractSpecVersion(artifacts.spec);
@@ -104,8 +109,10 @@ async function runPreflight({ args, options = {}, logger }) {
104
109
  version: specVersion,
105
110
  last_checkpoint: lastCheckpoint
106
111
  },
107
- architecture: { exists: artifacts.architecture.exists },
108
- implementation_plan: {
112
+ architecture: { exists: artifacts.architecture.exists },
113
+ design_doc: { exists: artifacts.design_doc.exists, path: artifacts.design_doc.path || null },
114
+ readiness: { exists: artifacts.readiness.exists, path: artifacts.readiness.path || null },
115
+ implementation_plan: {
109
116
  exists: artifacts.implementation_plan.exists,
110
117
  path: artifacts.implementation_plan.path || null,
111
118
  status: artifacts.implementation_plan.exists ? (artifacts.implementation_plan.frontmatter.status || null) : null
@@ -179,8 +186,10 @@ async function runPreflight({ args, options = {}, logger }) {
179
186
  slug
180
187
  ? [`spec-${slug}.md`, artifacts.spec.exists, specVersion ? `version: ${specVersion}${lastCheckpoint ? ', last: "' + lastCheckpoint + '"' : ''}` : null]
181
188
  : null,
182
- ['architecture.md', artifacts.architecture.exists, null],
183
- slug ? [`implementation-plan-${slug}.md`, artifacts.implementation_plan.exists, artifacts.implementation_plan.exists ? `status: ${artifacts.implementation_plan.frontmatter.status || 'unknown'}` : null] : null,
189
+ ['architecture.md', artifacts.architecture.exists, null],
190
+ ['design-doc.md', artifacts.design_doc.exists, classification === 'MICRO' ? 'SMALL/MEDIUM pre-dev only' : null],
191
+ ['readiness.md', artifacts.readiness.exists, classification === 'MICRO' ? 'SMALL/MEDIUM pre-dev only' : null],
192
+ slug ? [`implementation-plan-${slug}.md`, artifacts.implementation_plan.exists, artifacts.implementation_plan.exists ? `status: ${artifacts.implementation_plan.frontmatter.status || 'unknown'}` : null] : null,
184
193
  slug ? [`conformance-${slug}.yaml`, artifacts.conformance.exists, classification === 'SMALL' || classification === 'MICRO' ? 'MEDIUM only — not required' : null] : null
185
194
  ].filter(Boolean);
186
195