@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.
- package/CHANGELOG.md +26 -1
- package/docs/pt/living-memory/reflexao-in-harness.md +2 -0
- package/package.json +1 -1
- package/src/agents.js +23 -22
- package/src/cli.js +48 -20
- package/src/commands/agent-audit.js +189 -119
- package/src/commands/artifact-validate.js +31 -14
- package/src/commands/context-health.js +205 -36
- package/src/commands/devlog-process.js +35 -13
- package/src/commands/feature-close.js +36 -0
- package/src/commands/learning.js +98 -19
- package/src/commands/live.js +48 -22
- package/src/commands/memory-archive.js +193 -193
- package/src/commands/memory-reflect-commit.js +28 -4
- package/src/commands/memory-restore.js +177 -177
- package/src/commands/memory-search.js +135 -135
- package/src/commands/memory-trim.js +191 -0
- package/src/commands/preflight.js +16 -7
- package/src/commands/quality-audit.js +119 -0
- package/src/commands/skill-audit.js +200 -0
- package/src/commands/squad-playbook.js +100 -0
- package/src/commands/squad-role-scan.js +188 -0
- package/src/commands/state-save.js +9 -7
- package/src/commands/workflow-execute.js +172 -32
- package/src/commands/workflow-next.js +148 -40
- package/src/commands/workflow-status.js +54 -22
- package/src/constants.js +1 -0
- package/src/current-state-trim.js +170 -0
- package/src/handoff-contract.js +11 -6
- package/src/i18n/messages/en.js +25 -7
- package/src/i18n/messages/es.js +19 -5
- package/src/i18n/messages/fr.js +19 -5
- package/src/i18n/messages/pt-BR.js +25 -7
- package/src/learning-import-claude.js +218 -0
- package/src/learning-loop-engine.js +268 -254
- package/src/learning-loop-migration.js +177 -163
- package/src/learning-materialize.js +192 -0
- package/src/lib/quality/provider.js +132 -0
- package/src/lib/quality/report.js +82 -0
- package/src/lib/quality/result.js +185 -0
- package/src/memory-reflect-engine.js +10 -4
- package/src/parser.js +5 -4
- package/src/preflight-engine.js +49 -22
- package/src/runtime-store.js +2 -1
- package/template/.aioson/agents/analyst.md +18 -6
- package/template/.aioson/agents/architect.md +3 -0
- package/template/.aioson/agents/committer.md +6 -6
- package/template/.aioson/agents/copywriter.md +27 -27
- package/template/.aioson/agents/dev.md +60 -41
- package/template/.aioson/agents/deyvin.md +44 -32
- package/template/.aioson/agents/discovery-design-doc.md +27 -13
- package/template/.aioson/agents/genome.md +81 -82
- package/template/.aioson/agents/manifests/dev.manifest.json +5 -4
- package/template/.aioson/agents/manifests/deyvin.manifest.json +4 -3
- package/template/.aioson/agents/neo.md +1 -1
- package/template/.aioson/agents/orchestrator.md +1 -1
- package/template/.aioson/agents/pentester.md +3 -2
- package/template/.aioson/agents/product.md +27 -19
- package/template/.aioson/agents/qa.md +8 -4
- package/template/.aioson/agents/setup.md +1 -1
- package/template/.aioson/agents/sheldon.md +1 -0
- package/template/.aioson/agents/site-forge.md +17 -19
- package/template/.aioson/agents/squad.md +4 -0
- package/template/.aioson/agents/tester.md +180 -153
- package/template/.aioson/agents/ux-ui.md +1 -1
- package/template/.aioson/config/autonomy-protocol.json +1 -0
- package/template/.aioson/config.md +12 -12
- package/template/.aioson/context/design-doc.md +136 -136
- package/template/.aioson/context/project-map.md +7 -5
- package/template/.aioson/context/seeds/seed-example.md +27 -27
- package/template/.aioson/context/user-profile.md +42 -42
- package/template/.aioson/design-docs/agent-loading-contract.md +117 -0
- package/template/.aioson/docs/dev/simple-plan-lane.md +92 -0
- package/template/.aioson/docs/product/conversation-playbook.md +15 -17
- package/template/.aioson/docs/quality/code-health-analysis.md +79 -0
- package/template/.aioson/docs/site-forge-build.md +2 -2
- package/template/.aioson/docs/site-forge-recon.md +5 -5
- package/template/.aioson/docs/squad/creation-flow.md +55 -0
- package/template/.aioson/docs/squad/eval-gate.md +79 -0
- package/template/.aioson/docs/squad/package-contract.md +39 -6
- package/template/.aioson/docs/squad/persona-grounding.md +62 -0
- package/template/.aioson/docs/squad/quality-lens.md +12 -1
- package/template/.aioson/genomes/INDEX.md +37 -37
- package/template/.aioson/genomes/copywriting/references/application-notes.md +2 -2
- package/template/.aioson/genomes/copywriting/references/frameworks/pms-research.md +1 -1
- package/template/.aioson/genomes/copywriting-brunson/references/application-notes.md +2 -2
- package/template/.aioson/learnings/gotchas/.gitkeep +1 -0
- package/template/.aioson/learnings/recipes/.gitkeep +1 -0
- package/template/.aioson/rules/agent-language-policy.md +21 -21
- package/template/.aioson/rules/agent-structural-contract.md +2 -2
- package/template/.aioson/rules/aioson-context-boundary.md +8 -6
- package/template/.aioson/rules/canonical-path-contract.md +10 -5
- package/template/.aioson/rules/data-format-convention.md +11 -11
- package/template/.aioson/rules/disk-first-artifacts.md +5 -4
- package/template/.aioson/rules/prd-section-ownership.md +12 -12
- package/template/.aioson/rules/simple-plan-lane.md +48 -0
- package/template/.aioson/rules/spec-level-ownership.md +5 -4
- package/template/.aioson/schemas/squad-blueprint.schema.json +32 -11
- package/template/.aioson/schemas/squad-manifest.schema.json +29 -8
- package/template/.aioson/skills/design/clean-saas-ui/SKILL.md +4 -4
- package/template/.aioson/skills/design/clean-saas-ui/references/art-direction.md +30 -30
- package/template/.aioson/skills/design/clean-saas-ui/references/motion.md +4 -4
- package/template/.aioson/skills/design/cognitive-core-ui/SKILL.md +2 -2
- package/template/.aioson/skills/design/cognitive-core-ui/references/design-tokens.md +1 -1
- package/template/.aioson/skills/design/cognitive-core-ui/references/patterns.md +1 -1
- package/template/.aioson/skills/design/neo-brutalist-ui/SKILL.md +5 -5
- package/template/.aioson/skills/design/pt.squarespace.com/references/components.md +2 -2
- package/template/.aioson/skills/design/pt.squarespace.com/references/websites.md +4 -4
- package/template/.aioson/skills/design-system/dashboards/SKILL.md +5 -5
- package/template/.aioson/skills/design-system/patterns/SKILL.md +1 -1
- package/template/.aioson/skills/marketing/references/cta-matrix.md +43 -43
- package/template/.aioson/skills/marketing/references/headline-matrix.md +33 -33
- package/template/.aioson/skills/marketing/references/market-intelligence.md +2 -2
- package/template/.aioson/skills/marketing/references/platform-constraints.md +2 -2
- package/template/.aioson/skills/marketing/references/pms-research.md +3 -3
- package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +7 -7
- package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +13 -11
- package/template/.aioson/skills/process/aioson-spec-driven/references/ui-language.md +85 -75
- package/template/.aioson/skills/process/decision-presentation/SKILL.md +11 -11
- package/template/.aioson/skills/process/decision-presentation/references/jargon-map.pt-BR.yaml +4 -4
- package/template/.aioson/skills/squad/references/executor-archetypes.md +77 -2
- package/template/.aioson/skills/static/harness-validate/SKILL.md +55 -46
- package/template/.aioson/skills/static/react-motion-patterns.md +1 -1
- package/template/.aioson/skills/static/static-html-patterns.md +2 -2
- package/template/.aioson/skills/static/threejs-patterns.md +2 -2
- package/template/.aioson/tasks/implementation-plan.md +325 -327
- package/template/.aioson/tasks/squad-analyze.md +93 -83
- package/template/.aioson/tasks/squad-create.md +156 -148
- package/template/.aioson/tasks/squad-design.md +223 -206
- package/template/.aioson/tasks/squad-eval.md +72 -0
- package/template/.aioson/tasks/squad-execution-plan.md +279 -279
- package/template/.aioson/tasks/squad-export.md +20 -20
- package/template/.aioson/tasks/squad-extend.md +73 -68
- package/template/.aioson/tasks/squad-investigate.md +57 -57
- package/template/.aioson/tasks/squad-pipeline.md +122 -122
- package/template/.aioson/tasks/squad-profile.md +48 -48
- package/template/.aioson/tasks/squad-refresh.md +242 -236
- package/template/.aioson/tasks/squad-repair.md +85 -85
- package/template/.aioson/tasks/squad-review.md +61 -61
- package/template/.aioson/tasks/squad-task-decompose.md +66 -66
- package/template/.aioson/tasks/squad-validate.md +65 -58
- package/template/.aioson/templates/squads/content-basic/template.json +1 -1
- package/template/.aioson/templates/squads/media-channel/template.json +1 -1
- package/template/.aioson/templates/squads/research-analysis/template.json +1 -1
- package/template/AGENTS.md +10 -6
- package/template/CLAUDE.md +10 -6
- package/template/OPENCODE.md +9 -5
- 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:
|
|
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
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/src/commands/learning.js
CHANGED
|
@@ -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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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 };
|