@jaimevalasek/aioson 1.28.1 → 1.30.0
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 +42 -0
- package/README.md +7 -5
- package/docs/en/5-reference/cli-reference.md +40 -10
- package/docs/pt/4-agentes/briefing.md +2 -0
- package/docs/pt/4-agentes/copywriter.md +2 -0
- package/docs/pt/4-agentes/genome.md +1 -0
- package/docs/pt/4-agentes/pm.md +1 -1
- package/docs/pt/4-agentes/profiler-enricher.md +2 -0
- package/docs/pt/4-agentes/profiler-forge.md +2 -0
- package/docs/pt/4-agentes/sheldon.md +2 -0
- package/docs/pt/4-agentes/squad.md +12 -10
- package/docs/pt/5-referencia/autopilot-handoff.md +4 -4
- package/docs/pt/5-referencia/comandos-cli.md +7 -3
- package/docs/pt/5-referencia/fluxo-artefatos.md +1 -1
- package/docs/pt/5-referencia/memoria-e-contexto.md +62 -2
- package/docs/pt/_arquivo/monitor-de-contexto.md +2 -2
- package/package.json +4 -2
- package/src/cli.js +72 -24
- package/src/commands/ac-test-audit.js +45 -0
- package/src/commands/artifact-validate.js +62 -50
- package/src/commands/classify.js +73 -2
- package/src/commands/context-brief.js +59 -0
- package/src/commands/context-guard.js +88 -0
- package/src/commands/context-monitor.js +1 -1
- package/src/commands/context-search.js +101 -52
- package/src/commands/context-select.js +11 -2
- package/src/commands/feature-archive.js +21 -12
- package/src/commands/feature-current.js +82 -0
- package/src/commands/gate-check.js +32 -15
- package/src/commands/harness-check.js +17 -1
- package/src/commands/hooks-install.js +169 -26
- package/src/commands/hygiene-scan.js +423 -0
- package/src/commands/rules-lint.js +124 -0
- package/src/commands/sdd-benchmark.js +134 -0
- package/src/commands/spec-analyze.js +6 -4
- package/src/commands/store-system.js +329 -49
- package/src/constants.js +8 -3
- package/src/context-brief.js +585 -0
- package/src/context-guard.js +209 -0
- package/src/context-search.js +796 -96
- package/src/context-selector.js +802 -420
- package/src/handoff-contract.js +14 -6
- package/src/harness/contract-schema.js +1 -1
- package/src/i18n/messages/en.js +12 -5
- package/src/i18n/messages/es.js +11 -4
- package/src/i18n/messages/fr.js +11 -4
- package/src/i18n/messages/pt-BR.js +12 -5
- package/src/lib/ac-test-audit.js +194 -0
- package/src/preflight-engine.js +10 -6
- package/src/squad/state-manager.js +1 -1
- package/template/.aioson/agents/analyst.md +93 -53
- package/template/.aioson/agents/architect.md +41 -32
- package/template/.aioson/agents/briefing-refiner.md +15 -2
- package/template/.aioson/agents/briefing.md +105 -86
- package/template/.aioson/agents/committer.md +1 -1
- package/template/.aioson/agents/copywriter.md +53 -10
- package/template/.aioson/agents/design-hybrid-forge.md +9 -5
- package/template/.aioson/agents/dev.md +22 -25
- package/template/.aioson/agents/deyvin.md +126 -124
- package/template/.aioson/agents/discover.md +8 -9
- package/template/.aioson/agents/discovery-design-doc.md +52 -36
- package/template/.aioson/agents/forge-run.md +3 -0
- package/template/.aioson/agents/genome.md +12 -6
- package/template/.aioson/agents/neo.md +30 -24
- package/template/.aioson/agents/orache.md +16 -21
- package/template/.aioson/agents/orchestrator.md +40 -31
- package/template/.aioson/agents/pentester.md +22 -12
- package/template/.aioson/agents/pm.md +11 -2
- package/template/.aioson/agents/product.md +162 -183
- package/template/.aioson/agents/profiler-enricher.md +29 -6
- package/template/.aioson/agents/profiler-forge.md +16 -6
- package/template/.aioson/agents/profiler-researcher.md +10 -6
- package/template/.aioson/agents/qa.md +29 -19
- package/template/.aioson/agents/scope-check.md +14 -2
- package/template/.aioson/agents/sheldon.md +51 -21
- package/template/.aioson/agents/site-forge.md +4 -6
- package/template/.aioson/agents/squad.md +7 -12
- package/template/.aioson/agents/tester.md +40 -30
- package/template/.aioson/agents/ux-ui.md +56 -41
- package/template/.aioson/agents/validator.md +2 -2
- package/template/.aioson/config.md +4 -3
- package/template/.aioson/design-docs/agent-loading-contract.md +3 -3
- package/template/.aioson/docs/LAYERS.md +2 -0
- package/template/.aioson/docs/autonomy-protocol.md +7 -5
- package/template/.aioson/docs/autopilot-handoff.md +5 -3
- package/template/.aioson/docs/dev/execution-discipline.md +3 -0
- package/template/.aioson/docs/dev/simple-plan-lane.md +126 -77
- package/template/.aioson/docs/dev/stack-conventions.md +4 -1
- package/template/.aioson/docs/deyvin/continuity-recovery.md +21 -18
- package/template/.aioson/docs/deyvin/debugging-escalation.md +3 -0
- package/template/.aioson/docs/deyvin/pair-execution.md +3 -0
- package/template/.aioson/docs/deyvin/runtime-handoffs.md +6 -3
- package/template/.aioson/docs/dossier/agent-templates.md +3 -0
- package/template/.aioson/docs/dossier/schema.md +3 -0
- package/template/.aioson/docs/example-external-api-context.md +2 -0
- package/template/.aioson/docs/feature-expansion-taxonomy.md +53 -0
- package/template/.aioson/docs/handoff-persistence.md +95 -91
- package/template/.aioson/docs/pentester/app-playbooks.md +3 -0
- package/template/.aioson/docs/pentester/browser-dast-playbook.md +401 -398
- package/template/.aioson/docs/pentester/llm-supplychain.md +3 -0
- package/template/.aioson/docs/product/conversation-playbook.md +1 -1
- package/template/.aioson/docs/quality/code-health-analysis.md +2 -0
- package/template/.aioson/docs/sheldon/enrichment-paths.md +47 -1
- package/template/.aioson/docs/sheldon/harness-contract.md +26 -21
- package/template/.aioson/docs/sheldon/quality-lens.md +3 -0
- package/template/.aioson/docs/sheldon/research-loop.md +3 -0
- package/template/.aioson/docs/sheldon/web-intelligence.md +3 -0
- package/template/.aioson/docs/site-forge-build.md +4 -2
- package/template/.aioson/docs/site-forge-extraction.md +2 -0
- package/template/.aioson/docs/site-forge-qa.md +2 -0
- package/template/.aioson/docs/site-forge-recon.md +7 -5
- package/template/.aioson/docs/site-forge-transform.md +2 -0
- package/template/.aioson/docs/squad/content-output.md +3 -0
- package/template/.aioson/docs/squad/creation-flow.md +22 -1
- package/template/.aioson/docs/squad/domain-breadth.md +3 -0
- package/template/.aioson/docs/squad/domain-classification.md +3 -0
- package/template/.aioson/docs/squad/eval-gate.md +3 -0
- package/template/.aioson/docs/squad/genome-bindings.md +14 -0
- package/template/.aioson/docs/squad/package-contract.md +5 -0
- package/template/.aioson/docs/squad/persona-grounding.md +65 -62
- package/template/.aioson/docs/squad/quality-lens.md +3 -0
- package/template/.aioson/docs/squad/research-loop.md +3 -0
- package/template/.aioson/docs/squad/session-operations.md +3 -0
- package/template/.aioson/docs/squad/workflow-quality.md +3 -0
- package/template/.aioson/docs/tester/coverage-quality.md +4 -1
- package/template/.aioson/docs/ux-ui/design-execution.md +9 -7
- package/template/.aioson/rules/README.md +48 -2
- package/template/.aioson/rules/agent-language-policy.md +26 -21
- package/template/.aioson/rules/agent-structural-contract.md +168 -158
- package/template/.aioson/rules/aioson-context-boundary.md +7 -1
- package/template/.aioson/rules/canonical-path-contract.md +16 -10
- package/template/.aioson/rules/data-format-convention.md +17 -11
- package/template/.aioson/rules/disk-first-artifacts.md +12 -8
- package/template/.aioson/rules/example-monetary-values.md +4 -0
- package/template/.aioson/rules/implementation-structure-and-data-access.md +50 -0
- package/template/.aioson/rules/output-brevity.md +2 -0
- package/template/.aioson/rules/prd-section-ownership.md +17 -12
- package/template/.aioson/rules/security-baseline.md +8 -3
- package/template/.aioson/rules/simple-plan-lane.md +22 -5
- package/template/.aioson/rules/source-code-language-convention.md +34 -0
- package/template/.aioson/rules/spec-level-ownership.md +10 -5
- package/template/.aioson/rules/squad-driver-pattern.md +5 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/artifact-map.md +24 -23
- package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +4 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +2 -2
- package/template/.aioson/skills/process/aioson-spec-driven/references/qa.md +1 -1
- package/template/.aioson/skills/process/briefing-expansion-scout/SKILL.md +72 -0
- package/template/.aioson/skills/process/product-scope-expansion/SKILL.md +74 -0
- package/template/.aioson/skills/process/sheldon-expansion-audit/SKILL.md +67 -0
- package/template/.aioson/skills/static/context-budget-guide.md +1 -1
- package/template/.aioson/skills/static/multi-agent-patterns.md +5 -4
- package/template/.aioson/tasks/squad-create.md +11 -0
- package/template/.aioson/tasks/squad-design.md +3 -3
- package/template/AGENTS.md +36 -19
- package/template/CLAUDE.md +9 -5
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { buildGuardResponse } = require('../context-guard');
|
|
6
|
+
|
|
7
|
+
// `aioson context:guard [path] --tool=claude [--json]`
|
|
8
|
+
//
|
|
9
|
+
// Reference adapter for the operational retrieval loop. A harness hook pipes the
|
|
10
|
+
// pending tool event on stdin; the guard answers with a harness-shaped injection
|
|
11
|
+
// payload (or an empty object when no project rule is salient). Always exits 0 —
|
|
12
|
+
// it is advisory and must never block the host harness.
|
|
13
|
+
async function runContextGuard({ args, options = {}, logger }) {
|
|
14
|
+
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
15
|
+
const event = await resolveEvent(args, options);
|
|
16
|
+
|
|
17
|
+
let response;
|
|
18
|
+
try {
|
|
19
|
+
response = await buildGuardResponse(event || {}, targetDir, {
|
|
20
|
+
tool: options.tool || 'claude',
|
|
21
|
+
agent: options.agent || options.a || 'dev'
|
|
22
|
+
});
|
|
23
|
+
} catch {
|
|
24
|
+
// The guard is advisory and runs on the PreToolUse hot path. Any internal
|
|
25
|
+
// failure must surface as an empty injection ({}), never a non-hook envelope
|
|
26
|
+
// ({"ok":false,...}) on the hook's stdout channel.
|
|
27
|
+
response = {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const guard = response && response._guard;
|
|
31
|
+
|
|
32
|
+
if (options.json) {
|
|
33
|
+
// Keep the wire payload pristine — strip the internal observability field.
|
|
34
|
+
const { _guard, ...wire } = response;
|
|
35
|
+
return wire;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (guard && guard.injected) {
|
|
39
|
+
logger.log(`context:guard injected ${guard.rules.length} rule(s): ${guard.rules.join(', ')} (confidence ${guard.confidence})`);
|
|
40
|
+
} else {
|
|
41
|
+
logger.log('context:guard: no salient project rule for this change');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return response;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function resolveEvent(args, options) {
|
|
48
|
+
if (typeof options.event === 'string') return safeParse(options.event);
|
|
49
|
+
if (typeof options['event-file'] === 'string') {
|
|
50
|
+
try {
|
|
51
|
+
const raw = fs.readFileSync(path.resolve(process.cwd(), options['event-file']), 'utf8');
|
|
52
|
+
return safeParse(raw);
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return readStdinEvent();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function safeParse(text) {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(text);
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readStdinEvent() {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
if (process.stdin.isTTY) {
|
|
71
|
+
resolve(null);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
let data = '';
|
|
75
|
+
let settled = false;
|
|
76
|
+
const settle = (value) => {
|
|
77
|
+
if (settled) return;
|
|
78
|
+
settled = true;
|
|
79
|
+
resolve(value);
|
|
80
|
+
};
|
|
81
|
+
process.stdin.setEncoding('utf8');
|
|
82
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
83
|
+
process.stdin.on('end', () => settle(safeParse(data)));
|
|
84
|
+
process.stdin.on('error', () => settle(null));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { runContextGuard };
|
|
@@ -87,7 +87,7 @@ async function runContextMonitor({ args, options, logger }) {
|
|
|
87
87
|
if (!options.json) {
|
|
88
88
|
logger.log(` ${icon} Context: ${tokens.toLocaleString()} tokens (${pct}%) — ${zone.toUpperCase()}`);
|
|
89
89
|
if (zone === 'warning') {
|
|
90
|
-
logger.log(` Suggestion: /
|
|
90
|
+
logger.log(` Suggestion: /compact before next agent activation; use /clear only for a hard reset`);
|
|
91
91
|
} else if (zone === 'critical' || zone === 'overflow') {
|
|
92
92
|
logger.log(` Run: aioson context:health . for reduction options`);
|
|
93
93
|
}
|
|
@@ -1,50 +1,61 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const cwd =
|
|
9
|
-
const limit = Number(options.limit) || 10;
|
|
10
|
-
|
|
11
|
-
if (!query) {
|
|
12
|
-
logger.log('Usage: aioson context:search <
|
|
13
|
-
return { ok: false, error: 'missing_query' };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
logger
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { withIndex } = require('../context-search');
|
|
6
|
+
|
|
7
|
+
async function runContextSearch({ args, options, logger }) {
|
|
8
|
+
const { query, cwd } = resolveSearchTarget(args, options);
|
|
9
|
+
const limit = Number(options.limit) || 10;
|
|
10
|
+
|
|
11
|
+
if (!query) {
|
|
12
|
+
logger.log('Usage: aioson context:search [path] --query="<text>" [--agent=dev] [--mode=executing] [--task="<text>"] [--paths=src/**] [--intent=memory|feature|rules] [--limit=10]');
|
|
13
|
+
return { ok: false, error: 'missing_query' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const result = await withIndex(async (idx) => {
|
|
17
|
+
let index = null;
|
|
18
|
+
if (!options['no-index']) {
|
|
19
|
+
index = await idx.indexDirectory(cwd, {
|
|
20
|
+
force: Boolean(options.force || options.refresh)
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const search = idx.searchPackage(query, {
|
|
24
|
+
limit,
|
|
25
|
+
projectDir: cwd,
|
|
26
|
+
agent: options.agent,
|
|
27
|
+
mode: options.mode,
|
|
28
|
+
task: options.task || options.goal,
|
|
29
|
+
paths: options.paths || options.path,
|
|
30
|
+
intent: options.intent || options.intents,
|
|
31
|
+
source: options.source || options.sourceType || options['source-type']
|
|
32
|
+
});
|
|
33
|
+
return { ...search, index };
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (options.json) {
|
|
37
|
+
return { ok: true, ...result };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const results = result.results || [];
|
|
41
|
+
if (results.length === 0) {
|
|
42
|
+
logger.log(`No results for: ${query}`);
|
|
43
|
+
return { ok: true, ...result };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
logger.log(`\n Context search for: "${query}"\n`);
|
|
47
|
+
printBucket(logger, 'Must read', result.package.must_read);
|
|
48
|
+
printBucket(logger, 'Should read', result.package.should_read);
|
|
49
|
+
printBucket(logger, 'Maybe', result.package.maybe);
|
|
50
|
+
|
|
51
|
+
return { ok: true, ...result };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function runContextSearchIndex({ args, options, logger }) {
|
|
55
|
+
const cwd = path.resolve(process.cwd(), args[0] || options.cwd || '.');
|
|
56
|
+
const force = Boolean(options.force);
|
|
57
|
+
|
|
58
|
+
logger.log(`Indexing: ${cwd} ...`);
|
|
48
59
|
|
|
49
60
|
const result = await withIndex(async (idx) => {
|
|
50
61
|
const r = await idx.indexDirectory(cwd, { force });
|
|
@@ -59,8 +70,46 @@ async function runContextSearchIndex({ args, options, logger }) {
|
|
|
59
70
|
logger.log(` Indexed: ${result.indexed} files`);
|
|
60
71
|
logger.log(` Skipped: ${result.skipped} files (already indexed)`);
|
|
61
72
|
logger.log(` Total in index: ${result.stats.totalDocs} docs`);
|
|
62
|
-
|
|
63
|
-
return { ok: true, ...result };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
|
|
74
|
+
return { ok: true, ...result };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveSearchTarget(args, options = {}) {
|
|
78
|
+
let cwd = path.resolve(process.cwd(), options.cwd || '.');
|
|
79
|
+
let query = String(options.query || options.q || '').trim();
|
|
80
|
+
|
|
81
|
+
if (args.length > 0 && query) {
|
|
82
|
+
cwd = path.resolve(process.cwd(), args[0]);
|
|
83
|
+
} else if (args.length > 1 && pathExists(path.resolve(process.cwd(), args[0]))) {
|
|
84
|
+
cwd = path.resolve(process.cwd(), args[0]);
|
|
85
|
+
query = args.slice(1).join(' ').trim();
|
|
86
|
+
} else if (!query) {
|
|
87
|
+
query = args.join(' ').trim();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { cwd, query };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function pathExists(targetPath) {
|
|
94
|
+
try {
|
|
95
|
+
fs.accessSync(targetPath);
|
|
96
|
+
return true;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function printBucket(logger, title, items) {
|
|
103
|
+
if (!items || items.length === 0) return;
|
|
104
|
+
logger.log(` ${title}:`);
|
|
105
|
+
for (let i = 0; i < items.length; i++) {
|
|
106
|
+
const item = items[i];
|
|
107
|
+
logger.log(` ${i + 1}. ${item.title} (${item.source_type}, ${item.confidence})`);
|
|
108
|
+
logger.log(` ${item.relPath}`);
|
|
109
|
+
if (item.reason) logger.log(` reason: ${item.reason}`);
|
|
110
|
+
if (item.snippet) logger.log(` ${item.snippet.replace(/\n/g, ' ')}`);
|
|
111
|
+
logger.log('');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { runContextSearch, runContextSearchIndex, resolveSearchTarget };
|
|
@@ -10,7 +10,9 @@ async function runContextSelect({ args, options = {}, logger }) {
|
|
|
10
10
|
mode: options.mode || 'planning',
|
|
11
11
|
task: options.task || options.goal || '',
|
|
12
12
|
paths: options.paths || options.path || '',
|
|
13
|
-
feature: options.feature || options.slug || ''
|
|
13
|
+
feature: options.feature || options.slug || '',
|
|
14
|
+
semantic: options.semantic,
|
|
15
|
+
noSemantic: options.noSemantic || options['no-semantic']
|
|
14
16
|
});
|
|
15
17
|
|
|
16
18
|
if (options.json) return result;
|
|
@@ -19,7 +21,7 @@ async function runContextSelect({ args, options = {}, logger }) {
|
|
|
19
21
|
if (result.task) logger.log(`Task: ${result.task}`);
|
|
20
22
|
if (result.paths.length > 0) logger.log(`Paths: ${result.paths.join(', ')}`);
|
|
21
23
|
logger.log('Boundary: load only the selected files until the task, mode, feature, or touched paths change.');
|
|
22
|
-
if (result.selected.length === 0) {
|
|
24
|
+
if (result.selected.length === 0 && (!result.memory || result.memory.length === 0)) {
|
|
23
25
|
logger.log('No context files selected.');
|
|
24
26
|
return result;
|
|
25
27
|
}
|
|
@@ -28,6 +30,13 @@ async function runContextSelect({ args, options = {}, logger }) {
|
|
|
28
30
|
logger.log(`- ${item.path} [${item.surface}; ${item.load_tier}] ${item.reason}`);
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
if (result.memory && result.memory.length > 0) {
|
|
34
|
+
logger.log('Memory matches:');
|
|
35
|
+
for (const item of result.memory) {
|
|
36
|
+
logger.log(`- [${item.target_type}] ${item.target_id} ${item.reason}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
return result;
|
|
32
41
|
}
|
|
33
42
|
|
|
@@ -126,13 +126,26 @@ async function findSlugFiles(ctxDir, slug, otherSlugs = []) {
|
|
|
126
126
|
.filter((name) => !belongsToOtherSlug(name, slug, otherSlugs));
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
async function findArchivedFiles(archiveDir) {
|
|
130
|
-
const entries = await readDirSafe(archiveDir);
|
|
131
|
-
return entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
async function findArchivedFiles(archiveDir) {
|
|
130
|
+
const entries = await readDirSafe(archiveDir);
|
|
131
|
+
return entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function removeEmptyDirBestEffort(dir) {
|
|
135
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
136
|
+
try {
|
|
137
|
+
await fs.rmdir(dir);
|
|
138
|
+
return;
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if (!err || err.code === 'ENOENT' || err.code === 'ENOTEMPTY') return;
|
|
141
|
+
if (attempt === 3) return;
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Enumerate every artefact that belongs to a feature slug — the exact surface
|
|
136
149
|
* `feature:archive` would move — but as a pure read-only discovery, for
|
|
137
150
|
* non-destructive consumers (e.g. `feature:export`). Never mutates the tree.
|
|
138
151
|
*
|
|
@@ -552,11 +565,7 @@ async function runRestore({ slug, ctxDir, archiveDir, manifestPath, dryRun, json
|
|
|
552
565
|
dossierRestored = path.relative(ctxDir, dossierSourceDir);
|
|
553
566
|
}
|
|
554
567
|
|
|
555
|
-
|
|
556
|
-
await fs.rmdir(archiveDir);
|
|
557
|
-
} catch {
|
|
558
|
-
// Directory not empty (manual files) — leave it alone.
|
|
559
|
-
}
|
|
568
|
+
await removeEmptyDirBestEffort(archiveDir);
|
|
560
569
|
|
|
561
570
|
await updateManifest(manifestPath, { slug }, 'remove');
|
|
562
571
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* aioson feature:current — resolve the active feature slug deterministically.
|
|
5
|
+
*
|
|
6
|
+
* Single source of truth for "which feature is active right now", so every spec
|
|
7
|
+
* agent (product, sheldon, analyst, ux-ui, scope-check, discovery-design-doc)
|
|
8
|
+
* resolves the SAME {slug} instead of re-guessing and colliding on bare paths
|
|
9
|
+
* (design-doc.md, readiness.md, scope-check.md, ui-spec.md, sheldon-enrichment.md).
|
|
10
|
+
*
|
|
11
|
+
* Resolution order:
|
|
12
|
+
* 1. project-pulse.md `active_feature` (when set and not the `(none)` sentinel)
|
|
13
|
+
* 2. the single `in_progress` row in features.md (unambiguous fallback)
|
|
14
|
+
* 3. empty — genuine project-level work, no active feature
|
|
15
|
+
*
|
|
16
|
+
* When more than one feature is `in_progress`, the result is `ambiguous`: no slug
|
|
17
|
+
* is guessed, and the caller must disambiguate (ask the user) rather than
|
|
18
|
+
* silently overwrite another feature's artifact.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* aioson feature:current . # prints the slug (or nothing) to stdout
|
|
22
|
+
* aioson feature:current . --json # { ok, slug, source, ambiguous, candidates }
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
const { readProjectPulse, parseFeaturesMap, readFileSafe, contextDir } = require('../preflight-engine');
|
|
27
|
+
|
|
28
|
+
// Values that mean "no active feature" rather than a real slug.
|
|
29
|
+
const NONE_SENTINELS = new Set(['', '(none)', 'none', 'null', '-', 'n/a']);
|
|
30
|
+
|
|
31
|
+
function normalizeSlug(value) {
|
|
32
|
+
return String(value == null ? '' : value).trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isNone(value) {
|
|
36
|
+
return NONE_SENTINELS.has(normalizeSlug(value).toLowerCase());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function resolveActiveFeature(targetDir) {
|
|
40
|
+
// 1. project-pulse.md active_feature — the single source of truth maintained
|
|
41
|
+
// by pulse:update and reset by feature:close.
|
|
42
|
+
const pulse = await readProjectPulse(targetDir);
|
|
43
|
+
if (pulse && pulse.exists && !isNone(pulse.active_feature)) {
|
|
44
|
+
return { slug: normalizeSlug(pulse.active_feature), source: 'pulse', ambiguous: false, candidates: [] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 2. features.md — fall back to the unique in_progress row.
|
|
48
|
+
const featuresContent = await readFileSafe(path.join(contextDir(targetDir), 'features.md'));
|
|
49
|
+
const map = parseFeaturesMap(featuresContent);
|
|
50
|
+
const inProgress = [];
|
|
51
|
+
for (const [slug, status] of map.entries()) {
|
|
52
|
+
if (normalizeSlug(status).toLowerCase() === 'in_progress' && !inProgress.includes(slug)) {
|
|
53
|
+
inProgress.push(slug);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (inProgress.length === 1) {
|
|
57
|
+
return { slug: inProgress[0], source: 'features.md', ambiguous: false, candidates: [] };
|
|
58
|
+
}
|
|
59
|
+
if (inProgress.length > 1) {
|
|
60
|
+
// Ambiguous: more than one feature is open. The caller must disambiguate
|
|
61
|
+
// (ask the user) rather than silently colliding on a guessed slug.
|
|
62
|
+
return { slug: '', source: 'features.md', ambiguous: true, candidates: inProgress };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 3. No active feature — genuine project-level work.
|
|
66
|
+
return { slug: '', source: 'none', ambiguous: false, candidates: [] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function runFeatureCurrent({ args = [], options = {}, logger = console } = {}) {
|
|
70
|
+
const targetDir = args[0] || options.dir || '.';
|
|
71
|
+
const resolved = await resolveActiveFeature(targetDir);
|
|
72
|
+
const payload = { ok: true, ...resolved };
|
|
73
|
+
|
|
74
|
+
if (!options.json) {
|
|
75
|
+
// Plain mode prints ONLY the slug so `$(aioson feature:current .)` is
|
|
76
|
+
// directly usable in shell substitution; ambiguous/none print nothing.
|
|
77
|
+
if (resolved.slug) logger.log(resolved.slug);
|
|
78
|
+
}
|
|
79
|
+
return payload;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { runFeatureCurrent, resolveActiveFeature };
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* aioson gate:check . --feature=checkout --gate=C --json
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
const path = require('node:path');
|
|
16
|
-
const {
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
const { auditAcceptanceCriteriaTests } = require('../lib/ac-test-audit');
|
|
17
|
+
const {
|
|
17
18
|
contextDir,
|
|
18
19
|
readFileSafe,
|
|
19
20
|
fileExists,
|
|
@@ -104,7 +105,7 @@ async function checkGate(targetDir, slug, gateLetter) {
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
// Gate D: check for QA sign-off in spec
|
|
107
|
-
if (gateLetter === 'D') {
|
|
108
|
+
if (gateLetter === 'D') {
|
|
108
109
|
if (specContent && specContent.includes('## QA Sign-off')) {
|
|
109
110
|
// Check verdict
|
|
110
111
|
const passMatch = specContent.match(/\*\*Verdict:\*\*\s*(PASS|FAIL)/i);
|
|
@@ -130,12 +131,23 @@ async function checkGate(targetDir, slug, gateLetter) {
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
// Also check spec version for explicit gate_execution
|
|
133
|
-
if (gates.execution && gates.execution !== 'pending') {
|
|
134
|
-
const gateD = gates.execution;
|
|
135
|
-
evidence.push({ type: 'gate_field', field: 'gate_execution', value: gateD, ok: gateD === 'approved' });
|
|
136
|
-
if (gateD !== 'approved') missing.push(`gate_execution: ${gateD}`);
|
|
137
|
-
}
|
|
138
|
-
|
|
134
|
+
if (gates.execution && gates.execution !== 'pending') {
|
|
135
|
+
const gateD = gates.execution;
|
|
136
|
+
evidence.push({ type: 'gate_field', field: 'gate_execution', value: gateD, ok: gateD === 'approved' });
|
|
137
|
+
if (gateD !== 'approved') missing.push(`gate_execution: ${gateD}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const acAudit = await auditAcceptanceCriteriaTests(targetDir, slug);
|
|
141
|
+
evidence.push({
|
|
142
|
+
type: 'ac_test_audit',
|
|
143
|
+
ok: acAudit.ok,
|
|
144
|
+
summary: acAudit.summary,
|
|
145
|
+
missing: acAudit.missing
|
|
146
|
+
});
|
|
147
|
+
if (!acAudit.ok) {
|
|
148
|
+
missing.push(`AC test audit failed: missing tests for ${acAudit.missing.join(', ')}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
139
151
|
|
|
140
152
|
const allOk = missing.length === 0;
|
|
141
153
|
const result = allOk ? 'PASS' : 'BLOCKED';
|
|
@@ -231,15 +243,20 @@ async function runGateCheck({ args, options = {}, logger }) {
|
|
|
231
243
|
}
|
|
232
244
|
}
|
|
233
245
|
|
|
234
|
-
const qaEvidence = check.evidence.filter((e) => e.type === 'qa_signoff' || e.type === 'checkpoint' || e.type === 'gate_field');
|
|
246
|
+
const qaEvidence = check.evidence.filter((e) => e.type === 'qa_signoff' || e.type === 'checkpoint' || e.type === 'gate_field' || e.type === 'ac_test_audit');
|
|
235
247
|
if (qaEvidence.length > 0) {
|
|
236
248
|
for (const q of qaEvidence) {
|
|
237
249
|
const icon = q.ok ? ' ✓' : ' ✗';
|
|
238
|
-
if (q.type === 'qa_signoff') logger.log(`${icon} QA sign-off: ${q.exists === false ? 'missing' : `verdict ${q.verdict || 'unclear'}`}`);
|
|
239
|
-
if (q.type === 'checkpoint') logger.log(` ✓ last_checkpoint: "${q.value}"`);
|
|
240
|
-
if (q.type === 'gate_field') logger.log(`${icon} gate_execution: ${q.value}`);
|
|
241
|
-
|
|
242
|
-
|
|
250
|
+
if (q.type === 'qa_signoff') logger.log(`${icon} QA sign-off: ${q.exists === false ? 'missing' : `verdict ${q.verdict || 'unclear'}`}`);
|
|
251
|
+
if (q.type === 'checkpoint') logger.log(` ✓ last_checkpoint: "${q.value}"`);
|
|
252
|
+
if (q.type === 'gate_field') logger.log(`${icon} gate_execution: ${q.value}`);
|
|
253
|
+
if (q.type === 'ac_test_audit') {
|
|
254
|
+
const s = q.summary || {};
|
|
255
|
+
const missing = q.missing && q.missing.length ? ` (missing: ${q.missing.join(', ')})` : '';
|
|
256
|
+
logger.log(`${icon} AC test audit: ${s.covered || 0}/${s.acs_total || 0} covered${missing}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
243
260
|
|
|
244
261
|
logger.log('');
|
|
245
262
|
const resultIcon = check.result === 'PASS' ? '✓' : '✗';
|
|
@@ -97,6 +97,17 @@ async function runHarnessCheck({ args, options = {}, logger, t }) {
|
|
|
97
97
|
(c) => c && typeof c.verification === 'string' && c.verification.trim()
|
|
98
98
|
);
|
|
99
99
|
const skipped = criteria.length - executable.length;
|
|
100
|
+
const strict = Boolean(options.strict);
|
|
101
|
+
const binaryWithoutVerification = criteria.filter(
|
|
102
|
+
(c) => c && c.binary === true && !(typeof c.verification === 'string' && c.verification.trim())
|
|
103
|
+
);
|
|
104
|
+
const strictErrors = [];
|
|
105
|
+
if (strict && criteria.length > 0 && executable.length === 0) {
|
|
106
|
+
strictErrors.push('strict mode requires at least one executable verification criterion');
|
|
107
|
+
}
|
|
108
|
+
if (strict && binaryWithoutVerification.length > 0) {
|
|
109
|
+
strictErrors.push(`strict mode requires verification for binary criteria: ${binaryWithoutVerification.map((c) => c.id).join(', ')}`);
|
|
110
|
+
}
|
|
100
111
|
|
|
101
112
|
const checks = await runCriteria({ criteria, cwd: targetDir, timeoutMs });
|
|
102
113
|
const failed = checks.filter((c) => !c.ok);
|
|
@@ -111,14 +122,16 @@ async function runHarnessCheck({ args, options = {}, logger, t }) {
|
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
const report = {
|
|
114
|
-
ok: failed.length === 0,
|
|
125
|
+
ok: failed.length === 0 && strictErrors.length === 0,
|
|
115
126
|
slug,
|
|
116
127
|
checked_at: new Date().toISOString(),
|
|
128
|
+
strict,
|
|
117
129
|
criteria_total: criteria.length,
|
|
118
130
|
executable_total: executable.length,
|
|
119
131
|
passed: checks.length - failed.length,
|
|
120
132
|
failed: failed.length,
|
|
121
133
|
skipped_no_verification: skipped,
|
|
134
|
+
strict_errors: strictErrors,
|
|
122
135
|
checks
|
|
123
136
|
};
|
|
124
137
|
|
|
@@ -140,6 +153,9 @@ async function runHarnessCheck({ args, options = {}, logger, t }) {
|
|
|
140
153
|
}
|
|
141
154
|
|
|
142
155
|
logger.log(t('harness.check_header', { slug }) || `Harness check — ${slug}`);
|
|
156
|
+
for (const error of strictErrors) {
|
|
157
|
+
logger.log(` ✗ ${error}`);
|
|
158
|
+
}
|
|
143
159
|
if (executable.length === 0) {
|
|
144
160
|
logger.log(t('harness.check_no_executable', { total: criteria.length }) || ` No criteria with verification commands (${criteria.length} criteria total). @validator judges them all.`);
|
|
145
161
|
return report;
|