@jaimevalasek/aioson 1.21.0 → 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 (148) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/docs/pt/living-memory/reflexao-in-harness.md +2 -0
  3. package/package.json +1 -1
  4. package/src/agents.js +23 -22
  5. package/src/cli.js +48 -20
  6. package/src/commands/agent-audit.js +189 -119
  7. package/src/commands/artifact-validate.js +31 -14
  8. package/src/commands/context-health.js +205 -36
  9. package/src/commands/devlog-process.js +35 -13
  10. package/src/commands/feature-close.js +36 -0
  11. package/src/commands/learning.js +98 -19
  12. package/src/commands/live.js +48 -22
  13. package/src/commands/memory-archive.js +193 -193
  14. package/src/commands/memory-reflect-commit.js +28 -4
  15. package/src/commands/memory-restore.js +177 -177
  16. package/src/commands/memory-search.js +135 -135
  17. package/src/commands/memory-trim.js +191 -0
  18. package/src/commands/preflight.js +16 -7
  19. package/src/commands/quality-audit.js +119 -0
  20. package/src/commands/skill-audit.js +200 -0
  21. package/src/commands/squad-playbook.js +100 -0
  22. package/src/commands/squad-role-scan.js +188 -0
  23. package/src/commands/state-save.js +9 -7
  24. package/src/commands/workflow-execute.js +172 -32
  25. package/src/commands/workflow-next.js +148 -40
  26. package/src/commands/workflow-status.js +54 -22
  27. package/src/constants.js +1 -0
  28. package/src/current-state-trim.js +170 -0
  29. package/src/handoff-contract.js +11 -6
  30. package/src/i18n/messages/en.js +25 -7
  31. package/src/i18n/messages/es.js +19 -5
  32. package/src/i18n/messages/fr.js +19 -5
  33. package/src/i18n/messages/pt-BR.js +25 -7
  34. package/src/learning-import-claude.js +218 -0
  35. package/src/learning-loop-engine.js +268 -254
  36. package/src/learning-loop-migration.js +177 -163
  37. package/src/learning-materialize.js +192 -0
  38. package/src/lib/quality/provider.js +132 -0
  39. package/src/lib/quality/report.js +82 -0
  40. package/src/lib/quality/result.js +185 -0
  41. package/src/memory-reflect-engine.js +10 -4
  42. package/src/parser.js +5 -4
  43. package/src/preflight-engine.js +49 -22
  44. package/src/runtime-store.js +2 -1
  45. package/template/.aioson/agents/analyst.md +18 -6
  46. package/template/.aioson/agents/architect.md +3 -0
  47. package/template/.aioson/agents/committer.md +6 -6
  48. package/template/.aioson/agents/copywriter.md +27 -27
  49. package/template/.aioson/agents/dev.md +60 -41
  50. package/template/.aioson/agents/deyvin.md +44 -32
  51. package/template/.aioson/agents/discovery-design-doc.md +27 -13
  52. package/template/.aioson/agents/genome.md +81 -82
  53. package/template/.aioson/agents/manifests/dev.manifest.json +5 -4
  54. package/template/.aioson/agents/manifests/deyvin.manifest.json +4 -3
  55. package/template/.aioson/agents/neo.md +1 -1
  56. package/template/.aioson/agents/orchestrator.md +1 -1
  57. package/template/.aioson/agents/pentester.md +3 -2
  58. package/template/.aioson/agents/product.md +27 -19
  59. package/template/.aioson/agents/qa.md +8 -4
  60. package/template/.aioson/agents/setup.md +1 -1
  61. package/template/.aioson/agents/sheldon.md +1 -0
  62. package/template/.aioson/agents/site-forge.md +17 -19
  63. package/template/.aioson/agents/squad.md +4 -0
  64. package/template/.aioson/agents/tester.md +180 -153
  65. package/template/.aioson/agents/ux-ui.md +1 -1
  66. package/template/.aioson/config/autonomy-protocol.json +1 -0
  67. package/template/.aioson/config.md +12 -12
  68. package/template/.aioson/context/design-doc.md +136 -136
  69. package/template/.aioson/context/project-map.md +7 -5
  70. package/template/.aioson/context/seeds/seed-example.md +27 -27
  71. package/template/.aioson/context/user-profile.md +42 -42
  72. package/template/.aioson/design-docs/agent-loading-contract.md +117 -0
  73. package/template/.aioson/docs/dev/simple-plan-lane.md +92 -0
  74. package/template/.aioson/docs/product/conversation-playbook.md +15 -17
  75. package/template/.aioson/docs/quality/code-health-analysis.md +79 -0
  76. package/template/.aioson/docs/site-forge-build.md +2 -2
  77. package/template/.aioson/docs/site-forge-recon.md +5 -5
  78. package/template/.aioson/docs/squad/creation-flow.md +55 -0
  79. package/template/.aioson/docs/squad/eval-gate.md +79 -0
  80. package/template/.aioson/docs/squad/package-contract.md +39 -6
  81. package/template/.aioson/docs/squad/persona-grounding.md +62 -0
  82. package/template/.aioson/docs/squad/quality-lens.md +12 -1
  83. package/template/.aioson/genomes/INDEX.md +37 -37
  84. package/template/.aioson/genomes/copywriting/references/application-notes.md +2 -2
  85. package/template/.aioson/genomes/copywriting/references/frameworks/pms-research.md +1 -1
  86. package/template/.aioson/genomes/copywriting-brunson/references/application-notes.md +2 -2
  87. package/template/.aioson/learnings/gotchas/.gitkeep +1 -0
  88. package/template/.aioson/learnings/recipes/.gitkeep +1 -0
  89. package/template/.aioson/rules/agent-language-policy.md +21 -21
  90. package/template/.aioson/rules/agent-structural-contract.md +2 -2
  91. package/template/.aioson/rules/aioson-context-boundary.md +8 -6
  92. package/template/.aioson/rules/canonical-path-contract.md +10 -5
  93. package/template/.aioson/rules/data-format-convention.md +11 -11
  94. package/template/.aioson/rules/disk-first-artifacts.md +5 -4
  95. package/template/.aioson/rules/prd-section-ownership.md +12 -12
  96. package/template/.aioson/rules/simple-plan-lane.md +48 -0
  97. package/template/.aioson/rules/spec-level-ownership.md +5 -4
  98. package/template/.aioson/schemas/squad-blueprint.schema.json +32 -11
  99. package/template/.aioson/schemas/squad-manifest.schema.json +29 -8
  100. package/template/.aioson/skills/design/clean-saas-ui/SKILL.md +4 -4
  101. package/template/.aioson/skills/design/clean-saas-ui/references/art-direction.md +30 -30
  102. package/template/.aioson/skills/design/clean-saas-ui/references/motion.md +4 -4
  103. package/template/.aioson/skills/design/cognitive-core-ui/SKILL.md +2 -2
  104. package/template/.aioson/skills/design/cognitive-core-ui/references/design-tokens.md +1 -1
  105. package/template/.aioson/skills/design/cognitive-core-ui/references/patterns.md +1 -1
  106. package/template/.aioson/skills/design/neo-brutalist-ui/SKILL.md +5 -5
  107. package/template/.aioson/skills/design/pt.squarespace.com/references/components.md +2 -2
  108. package/template/.aioson/skills/design/pt.squarespace.com/references/websites.md +4 -4
  109. package/template/.aioson/skills/design-system/dashboards/SKILL.md +5 -5
  110. package/template/.aioson/skills/design-system/patterns/SKILL.md +1 -1
  111. package/template/.aioson/skills/marketing/references/cta-matrix.md +43 -43
  112. package/template/.aioson/skills/marketing/references/headline-matrix.md +33 -33
  113. package/template/.aioson/skills/marketing/references/market-intelligence.md +2 -2
  114. package/template/.aioson/skills/marketing/references/platform-constraints.md +2 -2
  115. package/template/.aioson/skills/marketing/references/pms-research.md +3 -3
  116. package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +7 -7
  117. package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +13 -11
  118. package/template/.aioson/skills/process/aioson-spec-driven/references/ui-language.md +85 -75
  119. package/template/.aioson/skills/process/decision-presentation/SKILL.md +11 -11
  120. package/template/.aioson/skills/process/decision-presentation/references/jargon-map.pt-BR.yaml +4 -4
  121. package/template/.aioson/skills/squad/references/executor-archetypes.md +77 -2
  122. package/template/.aioson/skills/static/harness-validate/SKILL.md +55 -46
  123. package/template/.aioson/skills/static/react-motion-patterns.md +1 -1
  124. package/template/.aioson/skills/static/static-html-patterns.md +2 -2
  125. package/template/.aioson/skills/static/threejs-patterns.md +2 -2
  126. package/template/.aioson/tasks/implementation-plan.md +325 -327
  127. package/template/.aioson/tasks/squad-analyze.md +93 -83
  128. package/template/.aioson/tasks/squad-create.md +156 -148
  129. package/template/.aioson/tasks/squad-design.md +223 -206
  130. package/template/.aioson/tasks/squad-eval.md +72 -0
  131. package/template/.aioson/tasks/squad-execution-plan.md +279 -279
  132. package/template/.aioson/tasks/squad-export.md +20 -20
  133. package/template/.aioson/tasks/squad-extend.md +73 -68
  134. package/template/.aioson/tasks/squad-investigate.md +57 -57
  135. package/template/.aioson/tasks/squad-pipeline.md +122 -122
  136. package/template/.aioson/tasks/squad-profile.md +48 -48
  137. package/template/.aioson/tasks/squad-refresh.md +242 -236
  138. package/template/.aioson/tasks/squad-repair.md +85 -85
  139. package/template/.aioson/tasks/squad-review.md +61 -61
  140. package/template/.aioson/tasks/squad-task-decompose.md +66 -66
  141. package/template/.aioson/tasks/squad-validate.md +65 -58
  142. package/template/.aioson/templates/squads/content-basic/template.json +1 -1
  143. package/template/.aioson/templates/squads/media-channel/template.json +1 -1
  144. package/template/.aioson/templates/squads/research-analysis/template.json +1 -1
  145. package/template/AGENTS.md +10 -6
  146. package/template/CLAUDE.md +10 -6
  147. package/template/OPENCODE.md +9 -5
  148. package/template/agents/_shared/learning-capture-directive.md +88 -0
@@ -70,8 +70,9 @@ async function runArtifactValidate({ args, options = {}, logger }) {
70
70
  reqCount = `${new Set(reqs).size} REQs, ${new Set(acs).size} ACs`;
71
71
  }
72
72
 
73
- // Conformance required?
74
- const conformanceRequired = classification === 'MEDIUM';
73
+ // Conformance required?
74
+ const conformanceRequired = classification === 'MEDIUM';
75
+ const designDocRequired = classification === 'SMALL' || classification === 'MEDIUM';
75
76
 
76
77
  // Build chain items
77
78
  const chain = [
@@ -110,15 +111,29 @@ async function runArtifactValidate({ args, options = {}, logger }) {
110
111
  required: true,
111
112
  indent: 1
112
113
  },
113
- {
114
- name: 'architecture.md',
115
- exists: artifacts.architecture.exists,
116
- detail: null,
117
- required: true,
118
- indent: 1
119
- },
120
- {
121
- name: `implementation-plan-${slug}.md`,
114
+ {
115
+ name: 'architecture.md',
116
+ exists: artifacts.architecture.exists,
117
+ detail: null,
118
+ required: true,
119
+ indent: 1
120
+ },
121
+ {
122
+ name: 'design-doc.md',
123
+ exists: artifacts.design_doc.exists,
124
+ detail: designDocRequired ? 'pre-dev design governance contract' : `SMALL/MEDIUM only — NOT required for ${classification || 'MICRO'}`,
125
+ required: designDocRequired,
126
+ indent: 1
127
+ },
128
+ {
129
+ name: 'readiness.md',
130
+ exists: artifacts.readiness.exists,
131
+ detail: designDocRequired ? 'pre-dev readiness contract' : `SMALL/MEDIUM only — NOT required for ${classification || 'MICRO'}`,
132
+ required: designDocRequired,
133
+ indent: 1
134
+ },
135
+ {
136
+ name: `implementation-plan-${slug}.md`,
122
137
  exists: artifacts.implementation_plan.exists,
123
138
  detail: planStatus ? `status: ${planStatus}` : null,
124
139
  required: true,
@@ -143,9 +158,11 @@ async function runArtifactValidate({ args, options = {}, logger }) {
143
158
  const ARTIFACT_OWNER_MAP = {
144
159
  'project.context.md': { agent: '@setup', reason: 'setup not complete' },
145
160
  [`prd-${slug}.md`]: { agent: '@product', reason: 'PRD not produced yet' },
146
- [`requirements-${slug}.md`]: { agent: '@analyst', reason: 'requirements not produced yet (Gate A)' },
147
- 'architecture.md': { agent: '@architect', reason: 'architecture not produced yet (Gate B)' },
148
- [`implementation-plan-${slug}.md`]: { agent: '@pm', reason: 'implementation plan not produced yet (Gate C)' },
161
+ [`requirements-${slug}.md`]: { agent: '@analyst', reason: 'requirements not produced yet (Gate A)' },
162
+ 'architecture.md': { agent: '@architect', reason: 'architecture not produced yet (Gate B)' },
163
+ 'design-doc.md': { agent: '@discovery-design-doc', reason: 'design governance contract not produced yet' },
164
+ 'readiness.md': { agent: '@discovery-design-doc', reason: 'readiness contract not produced yet' },
165
+ [`implementation-plan-${slug}.md`]: { agent: '@pm', reason: 'implementation plan not produced yet (Gate C)' },
149
166
  [`spec-${slug}.md`]: { agent: '@analyst', reason: 'spec not produced yet — @analyst seeds the feature memory' },
150
167
  [`conformance-${slug}.yaml`]: { agent: '@analyst', reason: 'conformance contract missing — @analyst creates it for MEDIUM features' }
151
168
  };
@@ -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;
@@ -84,9 +208,37 @@ async function runContextHealth({ args, options = {}, logger }) {
84
208
  } catch { /* skip unreadable files */ }
85
209
  }
86
210
 
211
+ // bootstrap/*.md is the per-activation memory layer: dev/qa/architect/deyvin
212
+ // read it on every session start, so it dominates the real activation cost.
213
+ // It lives in a subdir, so the top-level scan above missed it entirely —
214
+ // include it here so the heaviest layer is visible, not hidden (P0 of the
215
+ // agent-loading-contract). Backward-compatible: no bootstrap/ dir → no change.
216
+ const bootstrapDir = path.join(contextDir, 'bootstrap');
217
+ let bootstrapFiles = [];
218
+ try {
219
+ // Exclude *-archive.md: cold storage is never loaded at activation, so
220
+ // counting it would inflate the report and mislabel intended bulk as CRITICAL.
221
+ bootstrapFiles = (await fs.readdir(bootstrapDir))
222
+ .filter((f) => f.endsWith('.md') && !f.endsWith('-archive.md'));
223
+ } catch { /* no bootstrap dir — pre-Living-Memory projects */ }
224
+ for (const file of bootstrapFiles) {
225
+ try {
226
+ const content = await fs.readFile(path.join(bootstrapDir, file), 'utf8');
227
+ const tokens = estimateTokens(content);
228
+ totalTokens += tokens;
229
+ report.push({
230
+ file: `bootstrap/${file}`,
231
+ sizeBytes: content.length,
232
+ tokens,
233
+ heavy: tokens > HEAVY_TOKEN_THRESHOLD,
234
+ critical: tokens > CRITICAL_TOKEN_THRESHOLD
235
+ });
236
+ } catch { /* skip unreadable files */ }
237
+ }
238
+
87
239
  report.sort((a, b) => b.tokens - a.tokens);
88
240
 
89
- const doneFeatures = await loadFeatureStatuses(contextDir);
241
+ const doneFeatures = await loadFeatureStatuses(contextDir);
90
242
  const staleSpecs = report.filter((r) => {
91
243
  if (!r.file.startsWith('spec-')) return false;
92
244
  const slug = r.file.replace(/^spec-/, '').replace(/\.md$/, '');
@@ -109,17 +261,19 @@ async function runContextHealth({ args, options = {}, logger }) {
109
261
  db.close();
110
262
  }
111
263
 
112
- const skeletonPresent = entries.includes('skeleton-system.md') || entries.includes('skeleton.md');
113
-
114
- if (options.json) {
115
- return {
116
- ok: true,
117
- totalTokens,
118
- files: report,
119
- staleSpecs: staleSpecs.map((s) => s.file),
120
- cacheHitRate,
121
- skeletonPresent,
122
- 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
123
277
  };
124
278
  }
125
279
 
@@ -150,13 +304,18 @@ async function runContextHealth({ args, options = {}, logger }) {
150
304
  for (const r of heavyFiles) {
151
305
  const label = r.critical ? 'CRITICAL' : 'heavy';
152
306
  logger.log(`⚠ ${r.file} is ${label} (${formatBytes(r.sizeBytes)}). Consider:`);
153
- logger.log(` → Run: aioson context:pack . --scope=<feature>`);
154
- logger.log(` Creates a scoped context for a specific feature`);
307
+ if (r.file === 'bootstrap/current-state.md') {
308
+ logger.log(` Run: aioson memory:trim . --dry-run`);
309
+ logger.log(` Archives old log entries out of the hot bootstrap (every agent reads this at activation)`);
310
+ } else {
311
+ logger.log(` → Run: aioson context:pack . --scope=<feature>`);
312
+ logger.log(` Creates a scoped context for a specific feature`);
313
+ }
155
314
  }
156
315
  logger.log('');
157
316
  }
158
317
 
159
- if (staleSpecs.length > 0) {
318
+ if (staleSpecs.length > 0) {
160
319
  logger.log(`⚠ ${staleSpecs.length} stale spec file(s) (features: done):`);
161
320
  for (const s of staleSpecs) {
162
321
  const slug = s.file.replace(/^spec-/, '').replace(/\.md$/, '');
@@ -164,7 +323,16 @@ async function runContextHealth({ args, options = {}, logger }) {
164
323
  }
165
324
  logger.log(` Run: aioson feature:archive . --feature=<slug> to archive them`);
166
325
  logger.log('');
167
- }
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
+ }
168
336
 
169
337
  if (cacheHitRate !== null) {
170
338
  logger.log(`✓ Cache hit rate: ${cacheHitRate}% (last 7 days)`);
@@ -175,11 +343,12 @@ async function runContextHealth({ args, options = {}, logger }) {
175
343
 
176
344
  return {
177
345
  ok: true,
178
- totalTokens,
179
- files: report,
180
- staleSpecs: staleSpecs.map((s) => s.file),
181
- cacheHitRate,
182
- skeletonPresent,
346
+ totalTokens,
347
+ files: report,
348
+ staleSpecs: staleSpecs.map((s) => s.file),
349
+ driftWarnings,
350
+ cacheHitRate,
351
+ skeletonPresent,
183
352
  dbPath
184
353
  };
185
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 };
@@ -25,6 +25,12 @@ const { loadConfig } = require('../sub-task-engine');
25
25
  const { runDistillation, readFeatureClassification } = require('../learning-loop-engine');
26
26
  const { openRuntimeDb } = require('../runtime-store');
27
27
  const { runNotify } = require('./notify');
28
+ const { splitCurrentState, buildArchiveContent, parseActiveSlugs } = require('../current-state-trim');
29
+
30
+ // P0 agent-loading-contract: a feature closing is the natural cadence to roll
31
+ // aged-out current-state.md entries into the cold archive. Conservative window
32
+ // (gentle, automatic) — manual `memory:trim --keep=<N>` can trim harder.
33
+ const AUTO_CLOSE_KEEP = 25;
28
34
 
29
35
  function nowDate() {
30
36
  return new Date().toISOString().slice(0, 10);
@@ -531,6 +537,36 @@ async function runFeatureClose({ args, options = {}, logger }) {
531
537
  updates.push('distill: skipped (--no-distill flag)');
532
538
  }
533
539
 
540
+ // Auto-rollup bootstrap/current-state.md (P0 agent-loading-contract). The
541
+ // just-closed slug is already `done` in features.md, so it no longer counts as
542
+ // an active-slug exemption — its aged entries become eligible. Best-effort and
543
+ // non-blocking: a failure here must never break the closure. Opt out: --no-trim.
544
+ // SECURITY (TS-LC-02): the trim hook calls the engine directly, bypassing the
545
+ // AIOSON_RUNTIME_HOOK guard that memory:trim enforces. Honor that guard here
546
+ // too, so a tier-2 memory mutation never fires inside a hook/automation context.
547
+ const skipTrim = options['no-trim'] === true || options.trim === false
548
+ || process.env.AIOSON_RUNTIME_HOOK === '1';
549
+ if (verdict === 'PASS' && !skipTrim) {
550
+ try {
551
+ const csPath = path.join(targetDir, '.aioson/context/bootstrap/current-state.md');
552
+ const csContent = await readFileSafe(csPath);
553
+ if (csContent) {
554
+ const activeSlugs = parseActiveSlugs((await readFileSafe(path.join(targetDir, '.aioson/context/features.md'))) || '');
555
+ const split = splitCurrentState(csContent, { keep: AUTO_CLOSE_KEEP, activeSlugs });
556
+ if (split.ok && split.archivedEntries.length > 0) {
557
+ const archPath = path.join(targetDir, '.aioson/context/bootstrap/current-state-archive.md');
558
+ const eol = /\r\n/.test(csContent) ? '\r\n' : '\n';
559
+ const existingArchive = (await readFileSafe(archPath)) || '';
560
+ await fs.writeFile(archPath, buildArchiveContent(existingArchive, split.archivedEntries, nowDate(), eol), 'utf8');
561
+ await fs.writeFile(csPath, split.hotContent, 'utf8');
562
+ updates.push(`trim: archived ${split.archivedEntries.length} aged current-state entries (kept ${split.stats.kept})`);
563
+ }
564
+ }
565
+ } catch (err) {
566
+ updates.push(`trim: hook error (${(err && err.message) || err})`);
567
+ }
568
+ }
569
+
534
570
  const result = {
535
571
  ok: true,
536
572
  feature: slug,
@@ -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 };