@jaimevalasek/aioson 1.22.0 → 1.23.1
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 +932 -919
- package/docs/en/5-reference/cli-reference.md +85 -0
- package/docs/pt/4-agentes/pm.md +31 -4
- package/docs/pt/5-referencia/README.md +3 -0
- package/docs/pt/5-referencia/autopilot-handoff.md +131 -0
- package/docs/pt/5-referencia/comandos-cli.md +72 -6
- package/docs/pt/5-referencia/harness-retro.md +133 -0
- package/docs/pt/5-referencia/loop-guardrails.md +225 -0
- package/docs/pt/5-referencia/sdd-automation-scripts.md +25 -13
- package/package.json +1 -1
- package/src/agents.js +1 -1
- package/src/cli.js +70 -29
- package/src/commands/agent-epilogue.js +186 -0
- package/src/commands/context-select.js +33 -0
- package/src/commands/harness-preview.js +74 -0
- package/src/commands/harness-retro.js +221 -0
- package/src/commands/preflight-context.js +13 -9
- package/src/commands/review-cycle.js +328 -0
- package/src/commands/runtime.js +4 -4
- package/src/commands/self-implement-loop.js +12 -2
- package/src/commands/state-save.js +2 -0
- package/src/commands/workflow-execute.js +138 -28
- package/src/commands/workflow-next.js +11 -2
- package/src/commands/workflow-status.js +30 -10
- package/src/constants.js +15 -13
- package/src/context-memory.js +50 -25
- package/src/context-selector.js +394 -0
- package/src/harness/preview-artifact.js +85 -0
- package/src/i18n/messages/en.js +34 -7
- package/src/i18n/messages/es.js +34 -7
- package/src/i18n/messages/fr.js +34 -7
- package/src/i18n/messages/pt-BR.js +34 -7
- package/src/lib/retro/retro-aggregate.js +192 -0
- package/src/lib/retro/retro-render.js +185 -0
- package/src/lib/retro/retro-sources.js +624 -0
- package/src/parser.js +1 -1
- package/src/squad/preflight-context.js +26 -27
- package/template/.aioson/agents/analyst.md +41 -46
- package/template/.aioson/agents/architect.md +33 -46
- package/template/.aioson/agents/briefing.md +76 -67
- package/template/.aioson/agents/dev.md +73 -55
- package/template/.aioson/agents/deyvin.md +55 -50
- package/template/.aioson/agents/discovery-design-doc.md +35 -22
- package/template/.aioson/agents/manifests/architect.manifest.json +11 -1
- package/template/.aioson/agents/manifests/dev.manifest.json +15 -0
- package/template/.aioson/agents/manifests/pm.manifest.json +20 -0
- package/template/.aioson/agents/orchestrator.md +31 -18
- package/template/.aioson/agents/pentester.md +12 -4
- package/template/.aioson/agents/pm.md +41 -35
- package/template/.aioson/agents/product.md +116 -165
- package/template/.aioson/agents/qa.md +44 -13
- package/template/.aioson/agents/scope-check.md +46 -24
- package/template/.aioson/agents/sheldon.md +13 -0
- package/template/.aioson/agents/tester.md +28 -5
- package/template/.aioson/agents/ux-ui.md +36 -31
- package/template/.aioson/agents/validator.md +10 -2
- package/template/.aioson/config/autonomy-protocol.json +7 -0
- package/template/.aioson/design-docs/code-reuse.md +10 -5
- package/template/.aioson/design-docs/componentization.md +10 -5
- package/template/.aioson/design-docs/file-size.md +10 -5
- package/template/.aioson/design-docs/folder-structure.md +10 -5
- package/template/.aioson/design-docs/naming.md +10 -5
- package/template/.aioson/docs/autonomy-protocol.md +2 -2
- package/template/.aioson/docs/autopilot-handoff.md +82 -34
- package/template/.aioson/docs/briefing/briefing-craft.md +9 -3
- package/template/.aioson/docs/deyvin/continuity-recovery.md +18 -22
- package/template/.aioson/docs/product/conversation-playbook.md +8 -3
- package/template/.aioson/docs/product/prd-contract.md +8 -3
- package/template/.aioson/docs/product/quality-lens.md +8 -3
- package/template/.aioson/docs/product/research-loop.md +8 -3
- package/template/.aioson/docs/ux-ui/accessibility-audit.md +7 -2
- package/template/.aioson/docs/ux-ui/audit-mode.md +7 -2
- package/template/.aioson/docs/ux-ui/component-map.md +7 -2
- package/template/.aioson/docs/ux-ui/design-execution.md +7 -2
- package/template/.aioson/docs/ux-ui/design-gate.md +7 -2
- package/template/.aioson/docs/ux-ui/research-mode.md +7 -2
- package/template/.aioson/docs/ux-ui/site-delivery.md +7 -2
- package/template/.aioson/docs/ux-ui/token-contract.md +7 -2
- package/template/.aioson/rules/aioson-context-boundary.md +11 -9
- package/template/.aioson/rules/disk-first-artifacts.md +1 -1
- package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +1 -1
- package/template/.aioson/skills/process/aioson-spec-driven/references/architect.md +3 -2
- package/template/.aioson/skills/process/aioson-spec-driven/references/artifact-map.md +21 -9
- package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +2 -1
- package/template/.aioson/skills/process/aioson-spec-driven/references/pm.md +2 -1
- package/template/.aioson/skills/static/web-research-cache.md +29 -8
- package/template/AGENTS.md +1 -1
- package/template/CLAUDE.md +1 -1
|
@@ -268,20 +268,33 @@ function buildSuggestion({
|
|
|
268
268
|
};
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
function timeSince(isoString) {
|
|
272
|
-
const now = Date.now();
|
|
273
|
-
const then = new Date(isoString).getTime();
|
|
274
|
-
const diffMs = now - then;
|
|
271
|
+
function timeSince(isoString) {
|
|
272
|
+
const now = Date.now();
|
|
273
|
+
const then = new Date(isoString).getTime();
|
|
274
|
+
const diffMs = now - then;
|
|
275
275
|
const minutes = Math.floor(diffMs / 60000);
|
|
276
276
|
if (minutes < 1) return 'just now';
|
|
277
277
|
if (minutes < 60) return `${minutes}m`;
|
|
278
278
|
const hours = Math.floor(minutes / 60);
|
|
279
279
|
if (hours < 24) return `${hours}h`;
|
|
280
280
|
const days = Math.floor(hours / 24);
|
|
281
|
-
return `${days}d`;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
281
|
+
return `${days}d`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function handoffMatchesState(handoff, state) {
|
|
285
|
+
if (!handoff || !state) return false;
|
|
286
|
+
const stateMode = state.mode || null;
|
|
287
|
+
const handoffMode = handoff.workflow_mode || null;
|
|
288
|
+
if (stateMode && handoffMode && stateMode !== handoffMode) return false;
|
|
289
|
+
|
|
290
|
+
const stateFeature = state.featureSlug || null;
|
|
291
|
+
const handoffFeature = handoff.feature_slug || null;
|
|
292
|
+
if (stateFeature || handoffFeature) return stateFeature === handoffFeature;
|
|
293
|
+
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function runWorkflowStatus({ args, options, logger, t }) {
|
|
285
298
|
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
286
299
|
const tool = options.tool || 'codex';
|
|
287
300
|
|
|
@@ -311,8 +324,15 @@ async function runWorkflowStatus({ args, options, logger, t }) {
|
|
|
311
324
|
const focusStage = getFocusStage(state);
|
|
312
325
|
const queuedNextStage = getQueuedNextStage(state);
|
|
313
326
|
|
|
314
|
-
const
|
|
315
|
-
const
|
|
327
|
+
const rawHandoff = await readHandoff(targetDir);
|
|
328
|
+
const handoff = handoffMatchesState(rawHandoff, state) ? rawHandoff : null;
|
|
329
|
+
const rawHandoffProtocol = await readHandoffProtocol(targetDir);
|
|
330
|
+
const handoffProtocol = handoffMatchesState({
|
|
331
|
+
workflow_mode: rawHandoffProtocol && rawHandoffProtocol.workflow_mode,
|
|
332
|
+
feature_slug: rawHandoffProtocol && rawHandoffProtocol.feature_slug
|
|
333
|
+
}, state)
|
|
334
|
+
? rawHandoffProtocol
|
|
335
|
+
: null;
|
|
316
336
|
const artifacts = await buildKeyArtifacts(targetDir, state);
|
|
317
337
|
const squads = await scanSquads(targetDir);
|
|
318
338
|
const genomeCount = await scanGenomes(targetDir);
|
package/src/constants.js
CHANGED
|
@@ -225,7 +225,7 @@ const AGENT_DEFINITIONS = [
|
|
|
225
225
|
command: '@analyst',
|
|
226
226
|
path: '.aioson/agents/analyst.md',
|
|
227
227
|
dependsOn: ['.aioson/context/project.context.md'],
|
|
228
|
-
output: '.aioson/context/discovery.md'
|
|
228
|
+
output: '.aioson/context/discovery.md or .aioson/context/requirements-{slug}.md + .aioson/context/spec-{slug}.md'
|
|
229
229
|
},
|
|
230
230
|
{
|
|
231
231
|
id: 'scope-check',
|
|
@@ -246,10 +246,10 @@ const AGENT_DEFINITIONS = [
|
|
|
246
246
|
description: 'Project structure and technical decisions (SMALL/MEDIUM)',
|
|
247
247
|
command: '@architect',
|
|
248
248
|
path: '.aioson/agents/architect.md',
|
|
249
|
-
dependsOn: [
|
|
250
|
-
'.aioson/context/project.context.md',
|
|
251
|
-
'.aioson/context/discovery.md'
|
|
252
|
-
],
|
|
249
|
+
dependsOn: [
|
|
250
|
+
'.aioson/context/project.context.md',
|
|
251
|
+
'.aioson/context/discovery.md or .aioson/context/requirements-{slug}.md + .aioson/context/spec-{slug}.md'
|
|
252
|
+
],
|
|
253
253
|
output: '.aioson/context/architecture.md'
|
|
254
254
|
},
|
|
255
255
|
{
|
|
@@ -272,14 +272,16 @@ const AGENT_DEFINITIONS = [
|
|
|
272
272
|
description: 'Backlog and user stories (MEDIUM only)',
|
|
273
273
|
command: '@pm',
|
|
274
274
|
path: '.aioson/agents/pm.md',
|
|
275
|
-
dependsOn: [
|
|
276
|
-
'.aioson/context/project.context.md',
|
|
277
|
-
'.aioson/context/prd.md or .aioson/context/prd-{slug}.md',
|
|
278
|
-
'.aioson/context/
|
|
279
|
-
'.aioson/context/
|
|
280
|
-
'.aioson/context/
|
|
281
|
-
|
|
282
|
-
|
|
275
|
+
dependsOn: [
|
|
276
|
+
'.aioson/context/project.context.md',
|
|
277
|
+
'.aioson/context/prd.md or .aioson/context/prd-{slug}.md',
|
|
278
|
+
'.aioson/context/requirements-{slug}.md + .aioson/context/spec-{slug}.md (feature mode)',
|
|
279
|
+
'.aioson/context/discovery.md',
|
|
280
|
+
'.aioson/context/architecture.md',
|
|
281
|
+
'.aioson/context/design-doc-{slug}.md + .aioson/context/readiness-{slug}.md (feature mode, when present)',
|
|
282
|
+
'.aioson/context/ui-spec.md (when present)'
|
|
283
|
+
],
|
|
284
|
+
output: '.aioson/context/prd.md or prd-{slug}.md (enriched with acceptance criteria) + .aioson/context/implementation-plan-{slug}.md for MEDIUM features'
|
|
283
285
|
},
|
|
284
286
|
{
|
|
285
287
|
id: 'dev',
|
package/src/context-memory.js
CHANGED
|
@@ -790,9 +790,10 @@ async function writeDerivedContextMemory({
|
|
|
790
790
|
};
|
|
791
791
|
}
|
|
792
792
|
|
|
793
|
-
async function collectActiveDossiers(targetDir) {
|
|
794
|
-
const featuresDir = path.join(targetDir, CONTEXT_DIR, 'features');
|
|
795
|
-
|
|
793
|
+
async function collectActiveDossiers(targetDir) {
|
|
794
|
+
const featuresDir = path.join(targetDir, CONTEXT_DIR, 'features');
|
|
795
|
+
const activeFeatureSlugs = await readActiveFeatureSlugs(targetDir);
|
|
796
|
+
let slugs = [];
|
|
796
797
|
try {
|
|
797
798
|
const entries = await fs.readdir(featuresDir, { withFileTypes: true });
|
|
798
799
|
slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
@@ -809,10 +810,11 @@ async function collectActiveDossiers(targetDir) {
|
|
|
809
810
|
const updatedMatch = raw.match(/^last_updated_at:\s*(\S+)\s*$/m);
|
|
810
811
|
if (!statusMatch || statusMatch[1] !== 'active') continue;
|
|
811
812
|
const relPath = `${CONTEXT_DIR}/features/${slug}/dossier.md`;
|
|
812
|
-
active.push({
|
|
813
|
-
relPath,
|
|
814
|
-
slug,
|
|
815
|
-
|
|
813
|
+
active.push({
|
|
814
|
+
relPath,
|
|
815
|
+
slug,
|
|
816
|
+
isPointedActive: activeFeatureSlugs.has(slug),
|
|
817
|
+
lastUpdatedAt: updatedMatch ? updatedMatch[1] : null,
|
|
816
818
|
title: `Feature Dossier (${slug})`,
|
|
817
819
|
group: 'dossier',
|
|
818
820
|
readWhen: `active feature "${slug}" synthesis — why, what, code map, agent trail`,
|
|
@@ -832,24 +834,47 @@ async function collectActiveDossiers(targetDir) {
|
|
|
832
834
|
return b.lastUpdatedAt.localeCompare(a.lastUpdatedAt);
|
|
833
835
|
});
|
|
834
836
|
|
|
835
|
-
return active;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
function
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
if (
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
837
|
+
return active;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function extractFrontmatterValue(content, key) {
|
|
841
|
+
const re = new RegExp(`^${key}:\\s*(.+?)\\s*$`, 'm');
|
|
842
|
+
const match = String(content || '').match(re);
|
|
843
|
+
if (!match) return '';
|
|
844
|
+
return match[1].trim().replace(/^["']|["']$/g, '');
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function normalizeFeaturePointer(value) {
|
|
848
|
+
const raw = String(value || '').trim();
|
|
849
|
+
if (!raw || raw === '(none)' || raw.toLowerCase() === 'none' || raw === '-') return '';
|
|
850
|
+
return raw;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async function readActiveFeatureSlugs(targetDir) {
|
|
854
|
+
const out = new Set();
|
|
855
|
+
const pulse = await readTextIfExists(path.join(targetDir, CONTEXT_DIR, 'project-pulse.md'));
|
|
856
|
+
const devState = await readTextIfExists(path.join(targetDir, CONTEXT_DIR, 'dev-state.md'));
|
|
857
|
+
const pulseFeature = normalizeFeaturePointer(extractFrontmatterValue(pulse, 'active_feature'));
|
|
858
|
+
const devFeature = normalizeFeaturePointer(extractFrontmatterValue(devState, 'active_feature'));
|
|
859
|
+
if (pulseFeature) out.add(pulseFeature);
|
|
860
|
+
if (devFeature) out.add(devFeature);
|
|
861
|
+
return out;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function rankDossier(dossier, { agent, goal }, rank) {
|
|
865
|
+
let score = 0;
|
|
866
|
+
const reasons = [];
|
|
867
|
+
const lookupText = `${normalizeForLookup(agent)} ${normalizeForLookup(goal)}`.trim();
|
|
868
|
+
if (dossier.isPointedActive) {
|
|
869
|
+
score += 70 - rank * 2;
|
|
870
|
+
reasons.push(`active feature pointer (${dossier.slug})`);
|
|
871
|
+
}
|
|
872
|
+
if (lookupText.includes(dossier.slug.replace(/-/g, ' '))) {
|
|
873
|
+
score += 75;
|
|
874
|
+
reasons.push(`matches feature slug (${dossier.slug})`);
|
|
875
|
+
}
|
|
876
|
+
return { score: Math.max(score, 0), reasons };
|
|
877
|
+
}
|
|
853
878
|
|
|
854
879
|
// SF-project-11: paths under these prefixes are NEVER returned in a context
|
|
855
880
|
// pack, regardless of catalog score. This enforces the dev.md HARD RULE
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const {
|
|
6
|
+
parseFrontmatter,
|
|
7
|
+
parseAgentList,
|
|
8
|
+
appliesToAgent,
|
|
9
|
+
readFileSafe,
|
|
10
|
+
readProjectPulse,
|
|
11
|
+
readDevState
|
|
12
|
+
} = require('./preflight-engine');
|
|
13
|
+
|
|
14
|
+
const VALID_MODES = new Set(['planning', 'executing']);
|
|
15
|
+
|
|
16
|
+
const SURFACES = [
|
|
17
|
+
{ key: 'rules', dir: path.join('.aioson', 'rules'), recursive: false, defaultTier: 'trigger' },
|
|
18
|
+
{ key: 'docs', dir: path.join('.aioson', 'docs'), recursive: true, defaultTier: 'trigger' },
|
|
19
|
+
{ key: 'design_governance', dir: path.join('.aioson', 'design-docs'), recursive: false, defaultTier: 'trigger' },
|
|
20
|
+
{ key: 'context', dir: path.join('.aioson', 'context'), recursive: false, defaultTier: 'trigger' },
|
|
21
|
+
{ key: 'bootstrap', dir: path.join('.aioson', 'context', 'bootstrap'), recursive: false, defaultTier: 'trigger' },
|
|
22
|
+
{ key: 'feature_dossier', dir: path.join('.aioson', 'context', 'features'), recursive: true, defaultTier: 'trigger' }
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const FOUNDATION_CONTEXT_BASENAMES = new Set([
|
|
26
|
+
'project.context.md',
|
|
27
|
+
'project-pulse.md',
|
|
28
|
+
'dev-state.md',
|
|
29
|
+
'memory-index.md'
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const UNIVERSAL_ALWAYS_CONTEXT_BASENAMES = new Set([
|
|
33
|
+
'project.context.md',
|
|
34
|
+
'project-pulse.md'
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const AGENT_ALWAYS_CONTEXT_BASENAMES = new Map([
|
|
38
|
+
['dev', new Set(['dev-state.md', 'memory-index.md'])],
|
|
39
|
+
['deyvin', new Set(['dev-state.md', 'memory-index.md'])]
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
function normalizeSlashes(value) {
|
|
43
|
+
return String(value || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeToken(value) {
|
|
47
|
+
return String(value || '')
|
|
48
|
+
.normalize('NFD')
|
|
49
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.replace(/[`*_]/g, '')
|
|
52
|
+
.replace(/[^a-z0-9/-]+/g, ' ')
|
|
53
|
+
.trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeFeaturePointer(value) {
|
|
57
|
+
const normalized = normalizeToken(value).replace(/\s+/g, '-');
|
|
58
|
+
if (!normalized || normalized === 'none' || normalized === '-none-' || normalized === '-') return '';
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseListValue(value) {
|
|
63
|
+
if (value === undefined || value === null) return [];
|
|
64
|
+
const raw = String(value).trim();
|
|
65
|
+
if (!raw || raw === '[]') return [];
|
|
66
|
+
if (raw.startsWith('[') && raw.endsWith(']')) {
|
|
67
|
+
return raw
|
|
68
|
+
.slice(1, -1)
|
|
69
|
+
.split(',')
|
|
70
|
+
.map((item) => item.trim().replace(/^["']|["']$/g, ''))
|
|
71
|
+
.filter(Boolean);
|
|
72
|
+
}
|
|
73
|
+
return raw
|
|
74
|
+
.split(',')
|
|
75
|
+
.map((item) => item.trim().replace(/^["']|["']$/g, ''))
|
|
76
|
+
.filter(Boolean);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function modeFromOptions(mode) {
|
|
80
|
+
const normalized = normalizeToken(mode || 'planning');
|
|
81
|
+
return VALID_MODES.has(normalized) ? normalized : 'planning';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function escapeRegex(value) {
|
|
85
|
+
return String(value).replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function globToRegex(glob) {
|
|
89
|
+
const normalized = normalizeSlashes(glob);
|
|
90
|
+
let out = '^';
|
|
91
|
+
for (let i = 0; i < normalized.length; i += 1) {
|
|
92
|
+
const char = normalized[i];
|
|
93
|
+
const next = normalized[i + 1];
|
|
94
|
+
if (char === '*' && next === '*') {
|
|
95
|
+
out += '.*';
|
|
96
|
+
i += 1;
|
|
97
|
+
} else if (char === '*') {
|
|
98
|
+
out += '[^/]*';
|
|
99
|
+
} else {
|
|
100
|
+
out += escapeRegex(char);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
out += '$';
|
|
104
|
+
return new RegExp(out);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function pathMatchesPattern(filePath, pattern) {
|
|
108
|
+
const file = normalizeSlashes(filePath);
|
|
109
|
+
const normalizedPattern = normalizeSlashes(pattern);
|
|
110
|
+
if (!file || !normalizedPattern) return false;
|
|
111
|
+
if (normalizedPattern.endsWith('/**')) {
|
|
112
|
+
const prefix = normalizedPattern.slice(0, -3);
|
|
113
|
+
return file === prefix || file.startsWith(`${prefix}/`);
|
|
114
|
+
}
|
|
115
|
+
if (!normalizedPattern.includes('*')) {
|
|
116
|
+
return file === normalizedPattern || file.startsWith(`${normalizedPattern}/`);
|
|
117
|
+
}
|
|
118
|
+
return globToRegex(normalizedPattern).test(file);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function splitOptionList(value) {
|
|
122
|
+
if (Array.isArray(value)) return value.map(String).filter(Boolean);
|
|
123
|
+
return String(value || '')
|
|
124
|
+
.split(',')
|
|
125
|
+
.map((item) => item.trim())
|
|
126
|
+
.filter(Boolean);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function walkMarkdown(rootDir, relDir, recursive) {
|
|
130
|
+
const absDir = path.join(rootDir, relDir);
|
|
131
|
+
const out = [];
|
|
132
|
+
let entries;
|
|
133
|
+
try {
|
|
134
|
+
entries = await fs.readdir(absDir, { withFileTypes: true });
|
|
135
|
+
} catch {
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
if (entry.name.startsWith('.')) continue;
|
|
141
|
+
const childRel = path.join(relDir, entry.name);
|
|
142
|
+
if (entry.isDirectory()) {
|
|
143
|
+
if (recursive) out.push(...await walkMarkdown(rootDir, childRel, recursive));
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
147
|
+
if (entry.name.toLowerCase() === 'readme.md') continue;
|
|
148
|
+
out.push(normalizeSlashes(childRel));
|
|
149
|
+
}
|
|
150
|
+
return out.sort();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function inferContextMetadata(relPath, fm) {
|
|
154
|
+
const base = path.basename(relPath);
|
|
155
|
+
const slugMatch = base.match(/^(prd|requirements|spec|design-doc|readiness|implementation-plan|ui-spec|scope-check)-(.+)\.md$/);
|
|
156
|
+
const tags = [];
|
|
157
|
+
let featureSlug = fm.feature_slug || fm.feature || '';
|
|
158
|
+
let loadTier = fm.load_tier || 'trigger';
|
|
159
|
+
|
|
160
|
+
if (FOUNDATION_CONTEXT_BASENAMES.has(base)) {
|
|
161
|
+
if (UNIVERSAL_ALWAYS_CONTEXT_BASENAMES.has(base)) {
|
|
162
|
+
loadTier = fm.load_tier || 'always';
|
|
163
|
+
}
|
|
164
|
+
tags.push('foundation');
|
|
165
|
+
}
|
|
166
|
+
if (slugMatch) {
|
|
167
|
+
tags.push(slugMatch[1], 'feature');
|
|
168
|
+
if (!featureSlug) featureSlug = slugMatch[2];
|
|
169
|
+
}
|
|
170
|
+
if (base === 'discovery.md') tags.push('discovery', 'project-memory', 'entities', 'business-rules');
|
|
171
|
+
if (base === 'architecture.md') tags.push('architecture', 'technical-design', 'module-boundary');
|
|
172
|
+
if (base === 'ui-spec.md') tags.push('ui-spec', 'ui', 'ux', 'frontend', 'visual-design');
|
|
173
|
+
if (base === 'scope-check.md') tags.push('scope-check', 'alignment', 'pre-dev');
|
|
174
|
+
if (relPath.includes('/bootstrap/')) tags.push('bootstrap');
|
|
175
|
+
if (relPath.includes('/features/') && base === 'dossier.md') {
|
|
176
|
+
tags.push('feature', 'dossier');
|
|
177
|
+
if (!featureSlug) {
|
|
178
|
+
const parts = relPath.split('/');
|
|
179
|
+
const index = parts.indexOf('features');
|
|
180
|
+
if (index !== -1) featureSlug = parts[index + 1] || '';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { tags, featureSlug, loadTier };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function collectCandidates(targetDir) {
|
|
188
|
+
const candidates = [];
|
|
189
|
+
|
|
190
|
+
for (const surface of SURFACES) {
|
|
191
|
+
const relPaths = await walkMarkdown(targetDir, surface.dir, surface.recursive);
|
|
192
|
+
for (const relPath of relPaths) {
|
|
193
|
+
if (surface.key === 'feature_dossier' && !relPath.endsWith('/dossier.md')) continue;
|
|
194
|
+
const absPath = path.join(targetDir, relPath);
|
|
195
|
+
const content = await readFileSafe(absPath);
|
|
196
|
+
if (!content) continue;
|
|
197
|
+
const stat = await fs.stat(absPath).catch(() => null);
|
|
198
|
+
const fm = parseFrontmatter(content);
|
|
199
|
+
const inferred = inferContextMetadata(relPath, fm);
|
|
200
|
+
const description = fm.description || fm.name || path.basename(relPath, '.md');
|
|
201
|
+
candidates.push({
|
|
202
|
+
path: relPath,
|
|
203
|
+
surface: surface.key,
|
|
204
|
+
size: stat ? stat.size : content.length,
|
|
205
|
+
frontmatter: fm,
|
|
206
|
+
description,
|
|
207
|
+
agents: parseAgentList(fm.agents),
|
|
208
|
+
modes: parseListValue(fm.modes),
|
|
209
|
+
taskTypes: parseListValue(fm.task_types || fm.taskTypes),
|
|
210
|
+
triggers: parseListValue(fm.triggers),
|
|
211
|
+
pathPatterns: parseListValue(fm.paths || fm.globs),
|
|
212
|
+
scope: fm.scope || '',
|
|
213
|
+
featureSlug: fm.feature_slug || fm.feature || inferred.featureSlug || '',
|
|
214
|
+
tags: [...new Set([...parseListValue(fm.tags), ...inferred.tags])],
|
|
215
|
+
loadTier: fm.load_tier || inferred.loadTier || surface.defaultTier
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return candidates;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function keywordMatches(haystack, needles) {
|
|
224
|
+
const normalizedHaystack = normalizeToken(haystack);
|
|
225
|
+
const haystackWords = new Set(normalizedHaystack.split(/\s+/).flatMap(wordVariants));
|
|
226
|
+
return needles.filter((needle) => {
|
|
227
|
+
const normalizedNeedle = normalizeToken(needle);
|
|
228
|
+
if (!normalizedNeedle) return false;
|
|
229
|
+
if (normalizedHaystack.includes(normalizedNeedle)) return true;
|
|
230
|
+
const words = normalizedNeedle.split(/\s+/).filter((word) => word.length >= 4);
|
|
231
|
+
if (words.length === 0) return false;
|
|
232
|
+
const hits = words.filter((word) => wordVariants(word).some((variant) => haystackWords.has(variant))).length;
|
|
233
|
+
return hits >= Math.min(2, words.length);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function wordVariants(word) {
|
|
238
|
+
const raw = String(word || '').trim();
|
|
239
|
+
if (!raw) return [];
|
|
240
|
+
const variants = new Set([raw]);
|
|
241
|
+
if (raw.endsWith('ing') && raw.length > 5) {
|
|
242
|
+
const stem = raw.slice(0, -3);
|
|
243
|
+
variants.add(stem);
|
|
244
|
+
variants.add(`${stem}e`);
|
|
245
|
+
}
|
|
246
|
+
if (raw.endsWith('s') && raw.length > 4) variants.add(raw.slice(0, -1));
|
|
247
|
+
return [...variants];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function scoreCandidate(candidate, context) {
|
|
251
|
+
const reasons = [];
|
|
252
|
+
let score = 0;
|
|
253
|
+
let effectiveLoadTier = candidate.loadTier;
|
|
254
|
+
const base = path.basename(candidate.path);
|
|
255
|
+
|
|
256
|
+
if (!appliesToAgent(candidate.frontmatter, context.agent)) return null;
|
|
257
|
+
|
|
258
|
+
if (candidate.modes.length > 0 && !candidate.modes.map(normalizeToken).includes(context.mode)) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
if (candidate.modes.length > 0) {
|
|
262
|
+
score += 5;
|
|
263
|
+
reasons.push(`mode:${context.mode}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const agentAlways = AGENT_ALWAYS_CONTEXT_BASENAMES.get(context.agent);
|
|
267
|
+
if (agentAlways && agentAlways.has(base)) {
|
|
268
|
+
effectiveLoadTier = 'always';
|
|
269
|
+
score += 100;
|
|
270
|
+
reasons.push('load_tier:always');
|
|
271
|
+
} else if (candidate.loadTier === 'always') {
|
|
272
|
+
score += 100;
|
|
273
|
+
reasons.push('load_tier:always');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const matchedPaths = [];
|
|
277
|
+
for (const requestedPath of context.paths) {
|
|
278
|
+
for (const pattern of candidate.pathPatterns) {
|
|
279
|
+
if (pathMatchesPattern(requestedPath, pattern)) matchedPaths.push(`${requestedPath}~${pattern}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (matchedPaths.length > 0) {
|
|
283
|
+
score += 10;
|
|
284
|
+
reasons.push(`paths:${matchedPaths.slice(0, 3).join(',')}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const activeFeature = context.feature || context.activeFeature || '';
|
|
288
|
+
if (candidate.featureSlug && activeFeature && candidate.featureSlug === activeFeature) {
|
|
289
|
+
score += 45;
|
|
290
|
+
reasons.push(`feature:${candidate.featureSlug}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (candidate.featureSlug && context.lookup.includes(normalizeToken(candidate.featureSlug).replace(/-/g, ' '))) {
|
|
294
|
+
score += 45;
|
|
295
|
+
reasons.push(`feature-mentioned:${candidate.featureSlug}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const matchedTaskTypes = keywordMatches(context.lookup, candidate.taskTypes);
|
|
299
|
+
if (matchedTaskTypes.length > 0) {
|
|
300
|
+
score += 40;
|
|
301
|
+
reasons.push(`task_types:${matchedTaskTypes.slice(0, 3).join(',')}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const matchedTriggers = keywordMatches(context.lookup, candidate.triggers);
|
|
305
|
+
if (matchedTriggers.length > 0) {
|
|
306
|
+
score += 40;
|
|
307
|
+
reasons.push(`triggers:${matchedTriggers.slice(0, 3).join(',')}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const matchedTags = keywordMatches(context.lookup, candidate.tags);
|
|
311
|
+
if (matchedTags.length > 0) {
|
|
312
|
+
score += 20;
|
|
313
|
+
reasons.push(`tags:${matchedTags.slice(0, 3).join(',')}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const descriptionHits = keywordMatches(context.lookup, [
|
|
317
|
+
candidate.description,
|
|
318
|
+
candidate.scope,
|
|
319
|
+
path.basename(candidate.path, '.md').replace(/-/g, ' ')
|
|
320
|
+
]);
|
|
321
|
+
if (descriptionHits.length > 0) {
|
|
322
|
+
score += 20;
|
|
323
|
+
reasons.push(`description:${descriptionHits.slice(0, 2).join(',')}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const threshold = effectiveLoadTier === 'justified' ? 50 : 30;
|
|
327
|
+
if (score < threshold) return null;
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
path: candidate.path,
|
|
331
|
+
surface: candidate.surface,
|
|
332
|
+
load_tier: effectiveLoadTier,
|
|
333
|
+
size: candidate.size,
|
|
334
|
+
score,
|
|
335
|
+
reason: reasons.join('; ')
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function selectContext(targetDir, options = {}) {
|
|
340
|
+
const agent = normalizeToken(options.agent || 'dev');
|
|
341
|
+
const mode = modeFromOptions(options.mode);
|
|
342
|
+
const task = String(options.task || options.goal || '').trim();
|
|
343
|
+
const paths = splitOptionList(options.paths || options.path).map(normalizeSlashes);
|
|
344
|
+
const feature = normalizeFeaturePointer(options.feature || options.slug || '');
|
|
345
|
+
|
|
346
|
+
const pulse = await readProjectPulse(targetDir);
|
|
347
|
+
const devState = await readDevState(targetDir);
|
|
348
|
+
const activeFeature = normalizeFeaturePointer(
|
|
349
|
+
feature || pulse.active_feature || devState.active_feature || ''
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const lookup = normalizeToken([
|
|
353
|
+
agent,
|
|
354
|
+
mode,
|
|
355
|
+
task,
|
|
356
|
+
paths.join(' '),
|
|
357
|
+
activeFeature
|
|
358
|
+
].filter(Boolean).join(' '));
|
|
359
|
+
|
|
360
|
+
const candidates = await collectCandidates(targetDir);
|
|
361
|
+
const selected = [];
|
|
362
|
+
for (const candidate of candidates) {
|
|
363
|
+
const scored = scoreCandidate(candidate, {
|
|
364
|
+
agent,
|
|
365
|
+
mode,
|
|
366
|
+
task,
|
|
367
|
+
paths,
|
|
368
|
+
feature,
|
|
369
|
+
activeFeature,
|
|
370
|
+
lookup
|
|
371
|
+
});
|
|
372
|
+
if (scored) selected.push(scored);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
selected.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
ok: true,
|
|
379
|
+
agent,
|
|
380
|
+
mode,
|
|
381
|
+
task,
|
|
382
|
+
paths,
|
|
383
|
+
feature: feature || null,
|
|
384
|
+
active_feature: activeFeature || null,
|
|
385
|
+
selected
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
module.exports = {
|
|
390
|
+
selectContext,
|
|
391
|
+
collectCandidates,
|
|
392
|
+
parseListValue,
|
|
393
|
+
pathMatchesPattern
|
|
394
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `previewArtifact` — preview + ponteiro para outputs grandes (requirements §3.3,
|
|
5
|
+
* REQ-10). Tema 2 (should-have) da Harness Retrospective Optimization.
|
|
6
|
+
*
|
|
7
|
+
* previewArtifact(content, { maxBytes = 8192, artifactPath, label, persist })
|
|
8
|
+
* → { preview, truncated, fullPath, totalBytes }
|
|
9
|
+
*
|
|
10
|
+
* - Persist-first: quando `artifactPath` é dado e `persist !== false`, grava o
|
|
11
|
+
* conteúdo INTEGRAL em disco ANTES de gerar o preview.
|
|
12
|
+
* - `content` ≤ maxBytes → preview = conteúdo integral, `truncated: false`.
|
|
13
|
+
* - `content` > maxBytes → preview = primeiros maxBytes cortados em boundary
|
|
14
|
+
* UTF-8 seguro + linha-ponteiro padrão.
|
|
15
|
+
* - Falha de escrita NÃO lança: retorna preview truncado + `fullPath: null` +
|
|
16
|
+
* aviso (best-effort, mesmo padrão de `attempt-artifacts.js`).
|
|
17
|
+
* - `persist: false` referencia um arquivo já persistido sem reescrevê-lo
|
|
18
|
+
* (modo leitura de `harness:preview`).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
|
|
24
|
+
const DEFAULT_MAX_BYTES = 8192;
|
|
25
|
+
|
|
26
|
+
/** Corta um Buffer UTF-8 em `maxBytes` sem quebrar caractere multi-byte (edge 10). */
|
|
27
|
+
function safeUtf8Slice(buf, maxBytes) {
|
|
28
|
+
if (buf.length <= maxBytes) return buf.toString('utf8');
|
|
29
|
+
let end = maxBytes;
|
|
30
|
+
// Recua enquanto estiver no meio de uma sequência (bytes de continuação 10xxxxxx).
|
|
31
|
+
while (end > 0 && (buf[end] & 0xc0) === 0x80) end -= 1;
|
|
32
|
+
return buf.slice(0, end).toString('utf8');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function previewArtifact(content, options = {}) {
|
|
36
|
+
// Coerção segura: content não-string vira string ('' para null/undefined).
|
|
37
|
+
let text;
|
|
38
|
+
if (typeof content === 'string') {
|
|
39
|
+
text = content;
|
|
40
|
+
} else if (content === null || content === undefined) {
|
|
41
|
+
text = '';
|
|
42
|
+
} else {
|
|
43
|
+
try {
|
|
44
|
+
text = String(content);
|
|
45
|
+
} catch {
|
|
46
|
+
return { preview: '', truncated: false, fullPath: null, totalBytes: 0, warning: 'content não coercível' };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let maxBytes = Number(options.maxBytes);
|
|
51
|
+
if (!Number.isInteger(maxBytes) || maxBytes <= 0) maxBytes = DEFAULT_MAX_BYTES;
|
|
52
|
+
|
|
53
|
+
const artifactPath = options.artifactPath || null;
|
|
54
|
+
const persist = options.persist !== false;
|
|
55
|
+
|
|
56
|
+
const buf = Buffer.from(text, 'utf8');
|
|
57
|
+
const totalBytes = buf.length;
|
|
58
|
+
|
|
59
|
+
// Persist-first: grava integral antes de qualquer preview.
|
|
60
|
+
let fullPath = artifactPath;
|
|
61
|
+
let warning = null;
|
|
62
|
+
if (artifactPath && persist) {
|
|
63
|
+
try {
|
|
64
|
+
fs.mkdirSync(path.dirname(artifactPath), { recursive: true });
|
|
65
|
+
fs.writeFileSync(artifactPath, text, 'utf8');
|
|
66
|
+
} catch (err) {
|
|
67
|
+
fullPath = null;
|
|
68
|
+
warning = `falha ao persistir ${artifactPath}: ${err.message}`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (totalBytes <= maxBytes) {
|
|
73
|
+
return { preview: text, truncated: false, fullPath, totalBytes, ...(warning ? { warning } : {}) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const cut = safeUtf8Slice(buf, maxBytes);
|
|
77
|
+
const pointer = fullPath
|
|
78
|
+
? `[preview: primeiros ${maxBytes} de ${totalBytes} bytes — completo em ${fullPath}]`
|
|
79
|
+
: `[preview: primeiros ${maxBytes} de ${totalBytes} bytes — conteúdo completo não persistido]`;
|
|
80
|
+
const preview = `${cut}\n${pointer}`;
|
|
81
|
+
|
|
82
|
+
return { preview, truncated: true, fullPath, totalBytes, ...(warning ? { warning } : {}) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { previewArtifact, DEFAULT_MAX_BYTES, _internal: { safeUtf8Slice } };
|