@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.
Files changed (155) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +7 -5
  3. package/docs/en/5-reference/cli-reference.md +40 -10
  4. package/docs/pt/4-agentes/briefing.md +2 -0
  5. package/docs/pt/4-agentes/copywriter.md +2 -0
  6. package/docs/pt/4-agentes/genome.md +1 -0
  7. package/docs/pt/4-agentes/pm.md +1 -1
  8. package/docs/pt/4-agentes/profiler-enricher.md +2 -0
  9. package/docs/pt/4-agentes/profiler-forge.md +2 -0
  10. package/docs/pt/4-agentes/sheldon.md +2 -0
  11. package/docs/pt/4-agentes/squad.md +12 -10
  12. package/docs/pt/5-referencia/autopilot-handoff.md +4 -4
  13. package/docs/pt/5-referencia/comandos-cli.md +7 -3
  14. package/docs/pt/5-referencia/fluxo-artefatos.md +1 -1
  15. package/docs/pt/5-referencia/memoria-e-contexto.md +62 -2
  16. package/docs/pt/_arquivo/monitor-de-contexto.md +2 -2
  17. package/package.json +4 -2
  18. package/src/cli.js +72 -24
  19. package/src/commands/ac-test-audit.js +45 -0
  20. package/src/commands/artifact-validate.js +62 -50
  21. package/src/commands/classify.js +73 -2
  22. package/src/commands/context-brief.js +59 -0
  23. package/src/commands/context-guard.js +88 -0
  24. package/src/commands/context-monitor.js +1 -1
  25. package/src/commands/context-search.js +101 -52
  26. package/src/commands/context-select.js +11 -2
  27. package/src/commands/feature-archive.js +21 -12
  28. package/src/commands/feature-current.js +82 -0
  29. package/src/commands/gate-check.js +32 -15
  30. package/src/commands/harness-check.js +17 -1
  31. package/src/commands/hooks-install.js +169 -26
  32. package/src/commands/hygiene-scan.js +423 -0
  33. package/src/commands/rules-lint.js +124 -0
  34. package/src/commands/sdd-benchmark.js +134 -0
  35. package/src/commands/spec-analyze.js +6 -4
  36. package/src/commands/store-system.js +329 -49
  37. package/src/constants.js +8 -3
  38. package/src/context-brief.js +585 -0
  39. package/src/context-guard.js +209 -0
  40. package/src/context-search.js +796 -96
  41. package/src/context-selector.js +802 -420
  42. package/src/handoff-contract.js +14 -6
  43. package/src/harness/contract-schema.js +1 -1
  44. package/src/i18n/messages/en.js +12 -5
  45. package/src/i18n/messages/es.js +11 -4
  46. package/src/i18n/messages/fr.js +11 -4
  47. package/src/i18n/messages/pt-BR.js +12 -5
  48. package/src/lib/ac-test-audit.js +194 -0
  49. package/src/preflight-engine.js +10 -6
  50. package/src/squad/state-manager.js +1 -1
  51. package/template/.aioson/agents/analyst.md +93 -53
  52. package/template/.aioson/agents/architect.md +41 -32
  53. package/template/.aioson/agents/briefing-refiner.md +15 -2
  54. package/template/.aioson/agents/briefing.md +105 -86
  55. package/template/.aioson/agents/committer.md +1 -1
  56. package/template/.aioson/agents/copywriter.md +53 -10
  57. package/template/.aioson/agents/design-hybrid-forge.md +9 -5
  58. package/template/.aioson/agents/dev.md +22 -25
  59. package/template/.aioson/agents/deyvin.md +126 -124
  60. package/template/.aioson/agents/discover.md +8 -9
  61. package/template/.aioson/agents/discovery-design-doc.md +52 -36
  62. package/template/.aioson/agents/forge-run.md +3 -0
  63. package/template/.aioson/agents/genome.md +12 -6
  64. package/template/.aioson/agents/neo.md +30 -24
  65. package/template/.aioson/agents/orache.md +16 -21
  66. package/template/.aioson/agents/orchestrator.md +40 -31
  67. package/template/.aioson/agents/pentester.md +22 -12
  68. package/template/.aioson/agents/pm.md +11 -2
  69. package/template/.aioson/agents/product.md +162 -183
  70. package/template/.aioson/agents/profiler-enricher.md +29 -6
  71. package/template/.aioson/agents/profiler-forge.md +16 -6
  72. package/template/.aioson/agents/profiler-researcher.md +10 -6
  73. package/template/.aioson/agents/qa.md +29 -19
  74. package/template/.aioson/agents/scope-check.md +14 -2
  75. package/template/.aioson/agents/sheldon.md +51 -21
  76. package/template/.aioson/agents/site-forge.md +4 -6
  77. package/template/.aioson/agents/squad.md +7 -12
  78. package/template/.aioson/agents/tester.md +40 -30
  79. package/template/.aioson/agents/ux-ui.md +56 -41
  80. package/template/.aioson/agents/validator.md +2 -2
  81. package/template/.aioson/config.md +4 -3
  82. package/template/.aioson/design-docs/agent-loading-contract.md +3 -3
  83. package/template/.aioson/docs/LAYERS.md +2 -0
  84. package/template/.aioson/docs/autonomy-protocol.md +7 -5
  85. package/template/.aioson/docs/autopilot-handoff.md +5 -3
  86. package/template/.aioson/docs/dev/execution-discipline.md +3 -0
  87. package/template/.aioson/docs/dev/simple-plan-lane.md +126 -77
  88. package/template/.aioson/docs/dev/stack-conventions.md +4 -1
  89. package/template/.aioson/docs/deyvin/continuity-recovery.md +21 -18
  90. package/template/.aioson/docs/deyvin/debugging-escalation.md +3 -0
  91. package/template/.aioson/docs/deyvin/pair-execution.md +3 -0
  92. package/template/.aioson/docs/deyvin/runtime-handoffs.md +6 -3
  93. package/template/.aioson/docs/dossier/agent-templates.md +3 -0
  94. package/template/.aioson/docs/dossier/schema.md +3 -0
  95. package/template/.aioson/docs/example-external-api-context.md +2 -0
  96. package/template/.aioson/docs/feature-expansion-taxonomy.md +53 -0
  97. package/template/.aioson/docs/handoff-persistence.md +95 -91
  98. package/template/.aioson/docs/pentester/app-playbooks.md +3 -0
  99. package/template/.aioson/docs/pentester/browser-dast-playbook.md +401 -398
  100. package/template/.aioson/docs/pentester/llm-supplychain.md +3 -0
  101. package/template/.aioson/docs/product/conversation-playbook.md +1 -1
  102. package/template/.aioson/docs/quality/code-health-analysis.md +2 -0
  103. package/template/.aioson/docs/sheldon/enrichment-paths.md +47 -1
  104. package/template/.aioson/docs/sheldon/harness-contract.md +26 -21
  105. package/template/.aioson/docs/sheldon/quality-lens.md +3 -0
  106. package/template/.aioson/docs/sheldon/research-loop.md +3 -0
  107. package/template/.aioson/docs/sheldon/web-intelligence.md +3 -0
  108. package/template/.aioson/docs/site-forge-build.md +4 -2
  109. package/template/.aioson/docs/site-forge-extraction.md +2 -0
  110. package/template/.aioson/docs/site-forge-qa.md +2 -0
  111. package/template/.aioson/docs/site-forge-recon.md +7 -5
  112. package/template/.aioson/docs/site-forge-transform.md +2 -0
  113. package/template/.aioson/docs/squad/content-output.md +3 -0
  114. package/template/.aioson/docs/squad/creation-flow.md +22 -1
  115. package/template/.aioson/docs/squad/domain-breadth.md +3 -0
  116. package/template/.aioson/docs/squad/domain-classification.md +3 -0
  117. package/template/.aioson/docs/squad/eval-gate.md +3 -0
  118. package/template/.aioson/docs/squad/genome-bindings.md +14 -0
  119. package/template/.aioson/docs/squad/package-contract.md +5 -0
  120. package/template/.aioson/docs/squad/persona-grounding.md +65 -62
  121. package/template/.aioson/docs/squad/quality-lens.md +3 -0
  122. package/template/.aioson/docs/squad/research-loop.md +3 -0
  123. package/template/.aioson/docs/squad/session-operations.md +3 -0
  124. package/template/.aioson/docs/squad/workflow-quality.md +3 -0
  125. package/template/.aioson/docs/tester/coverage-quality.md +4 -1
  126. package/template/.aioson/docs/ux-ui/design-execution.md +9 -7
  127. package/template/.aioson/rules/README.md +48 -2
  128. package/template/.aioson/rules/agent-language-policy.md +26 -21
  129. package/template/.aioson/rules/agent-structural-contract.md +168 -158
  130. package/template/.aioson/rules/aioson-context-boundary.md +7 -1
  131. package/template/.aioson/rules/canonical-path-contract.md +16 -10
  132. package/template/.aioson/rules/data-format-convention.md +17 -11
  133. package/template/.aioson/rules/disk-first-artifacts.md +12 -8
  134. package/template/.aioson/rules/example-monetary-values.md +4 -0
  135. package/template/.aioson/rules/implementation-structure-and-data-access.md +50 -0
  136. package/template/.aioson/rules/output-brevity.md +2 -0
  137. package/template/.aioson/rules/prd-section-ownership.md +17 -12
  138. package/template/.aioson/rules/security-baseline.md +8 -3
  139. package/template/.aioson/rules/simple-plan-lane.md +22 -5
  140. package/template/.aioson/rules/source-code-language-convention.md +34 -0
  141. package/template/.aioson/rules/spec-level-ownership.md +10 -5
  142. package/template/.aioson/rules/squad-driver-pattern.md +5 -0
  143. package/template/.aioson/skills/process/aioson-spec-driven/references/artifact-map.md +24 -23
  144. package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +4 -0
  145. package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +2 -2
  146. package/template/.aioson/skills/process/aioson-spec-driven/references/qa.md +1 -1
  147. package/template/.aioson/skills/process/briefing-expansion-scout/SKILL.md +72 -0
  148. package/template/.aioson/skills/process/product-scope-expansion/SKILL.md +74 -0
  149. package/template/.aioson/skills/process/sheldon-expansion-audit/SKILL.md +67 -0
  150. package/template/.aioson/skills/static/context-budget-guide.md +1 -1
  151. package/template/.aioson/skills/static/multi-agent-patterns.md +5 -4
  152. package/template/.aioson/tasks/squad-create.md +11 -0
  153. package/template/.aioson/tasks/squad-design.md +3 -3
  154. package/template/AGENTS.md +36 -19
  155. 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: /clear before next agent activation`);
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 path = require('node:path');
4
- const { withIndex } = require('../context-search');
5
-
6
- async function runContextSearch({ args, options, logger }) {
7
- const query = args[0] || options.query || '';
8
- const cwd = path.resolve(process.cwd(), options.cwd || '.');
9
- const limit = Number(options.limit) || 10;
10
-
11
- if (!query) {
12
- logger.log('Usage: aioson context:search <query> [--limit=10] [--cwd=.]');
13
- return { ok: false, error: 'missing_query' };
14
- }
15
-
16
- const results = await withIndex(async (idx) => {
17
- return idx.search(query, { limit, projectDir: cwd });
18
- });
19
-
20
- if (options.json) {
21
- return { ok: true, results };
22
- }
23
-
24
- if (results.length === 0) {
25
- logger.log(`No results for: ${query}`);
26
- return { ok: true, results: [] };
27
- }
28
-
29
- logger.log(`\n Search results for: "${query}"\n`);
30
- for (let i = 0; i < results.length; i++) {
31
- const r = results[i];
32
- logger.log(` ${i + 1}. ${r.title}`);
33
- logger.log(` ${r.relPath}`);
34
- if (r.snippet) {
35
- logger.log(` ${r.snippet.replace(/\n/g, ' ')}`);
36
- }
37
- logger.log('');
38
- }
39
-
40
- return { ok: true, results };
41
- }
42
-
43
- async function runContextSearchIndex({ args, options, logger }) {
44
- const cwd = path.resolve(process.cwd(), args[0] || '.');
45
- const force = Boolean(options.force);
46
-
47
- logger.log(`Indexing: ${cwd} ...`);
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
- module.exports = { runContextSearch, runContextSearchIndex };
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
- * Enumerate every artefact that belongs to a feature slug — the exact surface
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
- try {
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;