@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.
- package/CHANGELOG.md +1 -1
- package/package.json +1 -1
- package/src/agents.js +23 -22
- package/src/cli.js +43 -20
- package/src/commands/agent-audit.js +189 -119
- package/src/commands/artifact-validate.js +31 -14
- package/src/commands/context-health.js +170 -34
- package/src/commands/devlog-process.js +35 -13
- package/src/commands/learning.js +98 -19
- package/src/commands/live.js +48 -22
- 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/handoff-contract.js +11 -6
- package/src/i18n/messages/en.js +13 -7
- package/src/i18n/messages/es.js +7 -5
- package/src/i18n/messages/fr.js +7 -5
- package/src/i18n/messages/pt-BR.js +13 -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/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/committer.md +5 -5
- package/template/.aioson/agents/copywriter.md +27 -27
- package/template/.aioson/agents/dev.md +58 -39
- package/template/.aioson/agents/deyvin.md +43 -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 +2 -2
- package/template/.aioson/agents/product.md +27 -19
- package/template/.aioson/agents/qa.md +4 -4
- package/template/.aioson/agents/setup.md +1 -1
- package/template/.aioson/agents/site-forge.md +17 -19
- package/template/.aioson/agents/squad.md +4 -0
- package/template/.aioson/agents/tester.md +178 -153
- package/template/.aioson/agents/ux-ui.md +1 -1
- 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 -138
- 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/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
|
@@ -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;
|
|
@@ -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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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 };
|
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 };
|
package/src/commands/live.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1243
|
-
|
|
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:
|
|
1270
|
+
summary: `Auto-closed: ${closeReason}`,
|
|
1248
1271
|
eventType: 'session_closed',
|
|
1249
1272
|
phase: 'live',
|
|
1250
|
-
message: `
|
|
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
|
|
1279
|
+
goal: `Auto-closed (${closeReason})`
|
|
1257
1280
|
});
|
|
1258
1281
|
}
|
|
1259
1282
|
await clearAgentSession(runtimeDir, agentName);
|
|
1260
1283
|
if (!options.json) {
|
|
1261
|
-
|
|
1262
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|