@jaimevalasek/aioson 1.23.0 → 1.23.3

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 (82) hide show
  1. package/docs/en/5-reference/cli-reference.md +85 -0
  2. package/docs/pt/4-agentes/pm.md +31 -4
  3. package/docs/pt/5-referencia/README.md +3 -0
  4. package/docs/pt/5-referencia/autopilot-handoff.md +131 -0
  5. package/docs/pt/5-referencia/comandos-cli.md +72 -6
  6. package/docs/pt/5-referencia/harness-retro.md +133 -0
  7. package/docs/pt/5-referencia/loop-guardrails.md +225 -0
  8. package/docs/pt/5-referencia/sdd-automation-scripts.md +25 -13
  9. package/package.json +1 -1
  10. package/src/cli.js +54 -29
  11. package/src/commands/agent-epilogue.js +186 -0
  12. package/src/commands/context-select.js +34 -0
  13. package/src/commands/preflight-context.js +13 -9
  14. package/src/commands/review-cycle.js +328 -0
  15. package/src/commands/runtime.js +4 -4
  16. package/src/commands/state-save.js +2 -0
  17. package/src/commands/workflow-execute.js +138 -28
  18. package/src/commands/workflow-next.js +3 -2
  19. package/src/commands/workflow-status.js +30 -10
  20. package/src/constants.js +15 -13
  21. package/src/context-memory.js +50 -25
  22. package/src/context-selector.js +420 -0
  23. package/src/gateway-pointer-merge.js +25 -4
  24. package/src/i18n/messages/en.js +13 -7
  25. package/src/i18n/messages/es.js +13 -7
  26. package/src/i18n/messages/fr.js +13 -7
  27. package/src/i18n/messages/pt-BR.js +13 -7
  28. package/src/parser.js +1 -1
  29. package/src/squad/preflight-context.js +26 -27
  30. package/template/.aioson/agents/analyst.md +41 -46
  31. package/template/.aioson/agents/architect.md +33 -46
  32. package/template/.aioson/agents/briefing.md +76 -67
  33. package/template/.aioson/agents/dev.md +66 -59
  34. package/template/.aioson/agents/deyvin.md +124 -114
  35. package/template/.aioson/agents/discovery-design-doc.md +35 -22
  36. package/template/.aioson/agents/manifests/architect.manifest.json +11 -1
  37. package/template/.aioson/agents/manifests/dev.manifest.json +15 -0
  38. package/template/.aioson/agents/manifests/pm.manifest.json +20 -0
  39. package/template/.aioson/agents/orchestrator.md +31 -18
  40. package/template/.aioson/agents/pentester.md +7 -7
  41. package/template/.aioson/agents/pm.md +41 -35
  42. package/template/.aioson/agents/product.md +116 -165
  43. package/template/.aioson/agents/qa.md +21 -14
  44. package/template/.aioson/agents/scope-check.md +46 -24
  45. package/template/.aioson/agents/tester.md +12 -6
  46. package/template/.aioson/agents/ux-ui.md +36 -31
  47. package/template/.aioson/agents/validator.md +3 -3
  48. package/template/.aioson/config/autonomy-protocol.json +7 -0
  49. package/template/.aioson/design-docs/code-reuse.md +10 -5
  50. package/template/.aioson/design-docs/componentization.md +10 -5
  51. package/template/.aioson/design-docs/file-size.md +10 -5
  52. package/template/.aioson/design-docs/folder-structure.md +10 -5
  53. package/template/.aioson/design-docs/naming.md +10 -5
  54. package/template/.aioson/docs/autonomy-protocol.md +2 -2
  55. package/template/.aioson/docs/autopilot-handoff.md +32 -21
  56. package/template/.aioson/docs/briefing/briefing-craft.md +9 -3
  57. package/template/.aioson/docs/deyvin/continuity-recovery.md +18 -22
  58. package/template/.aioson/docs/product/conversation-playbook.md +8 -3
  59. package/template/.aioson/docs/product/prd-contract.md +8 -3
  60. package/template/.aioson/docs/product/quality-lens.md +8 -3
  61. package/template/.aioson/docs/product/research-loop.md +8 -3
  62. package/template/.aioson/docs/ux-ui/accessibility-audit.md +7 -2
  63. package/template/.aioson/docs/ux-ui/audit-mode.md +7 -2
  64. package/template/.aioson/docs/ux-ui/component-map.md +7 -2
  65. package/template/.aioson/docs/ux-ui/design-execution.md +7 -2
  66. package/template/.aioson/docs/ux-ui/design-gate.md +7 -2
  67. package/template/.aioson/docs/ux-ui/research-mode.md +7 -2
  68. package/template/.aioson/docs/ux-ui/site-delivery.md +7 -2
  69. package/template/.aioson/docs/ux-ui/token-contract.md +7 -2
  70. package/template/.aioson/rules/aioson-context-boundary.md +1 -1
  71. package/template/.aioson/rules/disk-first-artifacts.md +1 -1
  72. package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +9 -7
  73. package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +1 -1
  74. package/template/.aioson/skills/process/aioson-spec-driven/references/architect.md +3 -2
  75. package/template/.aioson/skills/process/aioson-spec-driven/references/artifact-map.md +21 -9
  76. package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +2 -1
  77. package/template/.aioson/skills/process/aioson-spec-driven/references/deyvin.md +19 -15
  78. package/template/.aioson/skills/process/aioson-spec-driven/references/pm.md +2 -1
  79. package/template/.aioson/skills/static/web-research-cache.md +29 -8
  80. package/template/AGENTS.md +13 -13
  81. package/template/CLAUDE.md +9 -9
  82. package/template/OPENCODE.md +3 -2
@@ -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
- async function runWorkflowStatus({ args, options, logger, t }) {
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 handoff = await readHandoff(targetDir);
315
- const handoffProtocol = await readHandoffProtocol(targetDir);
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/discovery.md',
279
- '.aioson/context/architecture.md',
280
- '.aioson/context/ui-spec.md (when present)'
281
- ],
282
- output: '.aioson/context/prd.md or prd-{slug}.md (enriched with delivery plan and acceptance criteria)'
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',
@@ -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
- let slugs = [];
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
- lastUpdatedAt: updatedMatch ? updatedMatch[1] : null,
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 rankDossier(dossier, { agent, goal }, rank) {
839
- // Base score: between PRD (45) and bootstrap (65). Most recent dossier gets 60.
840
- let score = 60 - rank * 5;
841
- const reasons = [`active feature dossier (${dossier.slug})`];
842
- const lookupText = `${normalizeForLookup(agent)} ${normalizeForLookup(goal)}`.trim();
843
- if (lookupText.includes(dossier.slug.replace(/-/g, ' '))) {
844
- score += 15;
845
- reasons.push('matches active feature slug');
846
- }
847
- if (/(dev|architect|qa|implement|feature|dossier)/.test(lookupText)) {
848
- score += 10;
849
- reasons.push('agent/goal matches dossier context');
850
- }
851
- return { score: Math.max(score, 0), reasons };
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,420 @@
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 ACTIVATION_ONLY_CONTEXT_PATHS = new Set([
33
+ '.aioson/context/project.context.md',
34
+ '.aioson/context/project-pulse.md',
35
+ '.aioson/context/dev-state.md'
36
+ ]);
37
+
38
+ const UNIVERSAL_ALWAYS_CONTEXT_BASENAMES = new Set([
39
+ 'project.context.md',
40
+ 'project-pulse.md'
41
+ ]);
42
+
43
+ const AGENT_ALWAYS_CONTEXT_BASENAMES = new Map([
44
+ ['dev', new Set(['dev-state.md', 'memory-index.md'])],
45
+ ['deyvin', new Set(['dev-state.md', 'memory-index.md'])]
46
+ ]);
47
+
48
+ function normalizeSlashes(value) {
49
+ return String(value || '').replace(/\\/g, '/').replace(/^\.\//, '');
50
+ }
51
+
52
+ function normalizeToken(value) {
53
+ return String(value || '')
54
+ .normalize('NFD')
55
+ .replace(/[\u0300-\u036f]/g, '')
56
+ .toLowerCase()
57
+ .replace(/[`*_]/g, '')
58
+ .replace(/[^a-z0-9/-]+/g, ' ')
59
+ .trim();
60
+ }
61
+
62
+ function normalizeFeaturePointer(value) {
63
+ const normalized = normalizeToken(value).replace(/\s+/g, '-');
64
+ if (!normalized || normalized === 'none' || normalized === '-none-' || normalized === '-') return '';
65
+ return normalized;
66
+ }
67
+
68
+ function isActivationOnlyTask(agent, mode, task) {
69
+ if (agent !== 'deyvin' || mode !== 'planning') return false;
70
+ const normalized = normalizeToken(task);
71
+ if (!normalized) return true;
72
+ return (
73
+ normalized.includes('agent activation') ||
74
+ normalized.includes('activation only') ||
75
+ normalized.includes('without concrete task') ||
76
+ normalized.includes('no concrete task')
77
+ );
78
+ }
79
+
80
+ function parseListValue(value) {
81
+ if (value === undefined || value === null) return [];
82
+ const raw = String(value).trim();
83
+ if (!raw || raw === '[]') return [];
84
+ if (raw.startsWith('[') && raw.endsWith(']')) {
85
+ return raw
86
+ .slice(1, -1)
87
+ .split(',')
88
+ .map((item) => item.trim().replace(/^["']|["']$/g, ''))
89
+ .filter(Boolean);
90
+ }
91
+ return raw
92
+ .split(',')
93
+ .map((item) => item.trim().replace(/^["']|["']$/g, ''))
94
+ .filter(Boolean);
95
+ }
96
+
97
+ function modeFromOptions(mode) {
98
+ const normalized = normalizeToken(mode || 'planning');
99
+ return VALID_MODES.has(normalized) ? normalized : 'planning';
100
+ }
101
+
102
+ function escapeRegex(value) {
103
+ return String(value).replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
104
+ }
105
+
106
+ function globToRegex(glob) {
107
+ const normalized = normalizeSlashes(glob);
108
+ let out = '^';
109
+ for (let i = 0; i < normalized.length; i += 1) {
110
+ const char = normalized[i];
111
+ const next = normalized[i + 1];
112
+ if (char === '*' && next === '*') {
113
+ out += '.*';
114
+ i += 1;
115
+ } else if (char === '*') {
116
+ out += '[^/]*';
117
+ } else {
118
+ out += escapeRegex(char);
119
+ }
120
+ }
121
+ out += '$';
122
+ return new RegExp(out);
123
+ }
124
+
125
+ function pathMatchesPattern(filePath, pattern) {
126
+ const file = normalizeSlashes(filePath);
127
+ const normalizedPattern = normalizeSlashes(pattern);
128
+ if (!file || !normalizedPattern) return false;
129
+ if (normalizedPattern.endsWith('/**')) {
130
+ const prefix = normalizedPattern.slice(0, -3);
131
+ return file === prefix || file.startsWith(`${prefix}/`);
132
+ }
133
+ if (!normalizedPattern.includes('*')) {
134
+ return file === normalizedPattern || file.startsWith(`${normalizedPattern}/`);
135
+ }
136
+ return globToRegex(normalizedPattern).test(file);
137
+ }
138
+
139
+ function splitOptionList(value) {
140
+ if (Array.isArray(value)) return value.map(String).filter(Boolean);
141
+ return String(value || '')
142
+ .split(',')
143
+ .map((item) => item.trim())
144
+ .filter(Boolean);
145
+ }
146
+
147
+ async function walkMarkdown(rootDir, relDir, recursive) {
148
+ const absDir = path.join(rootDir, relDir);
149
+ const out = [];
150
+ let entries;
151
+ try {
152
+ entries = await fs.readdir(absDir, { withFileTypes: true });
153
+ } catch {
154
+ return out;
155
+ }
156
+
157
+ for (const entry of entries) {
158
+ if (entry.name.startsWith('.')) continue;
159
+ const childRel = path.join(relDir, entry.name);
160
+ if (entry.isDirectory()) {
161
+ if (recursive) out.push(...await walkMarkdown(rootDir, childRel, recursive));
162
+ continue;
163
+ }
164
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
165
+ if (entry.name.toLowerCase() === 'readme.md') continue;
166
+ out.push(normalizeSlashes(childRel));
167
+ }
168
+ return out.sort();
169
+ }
170
+
171
+ function inferContextMetadata(relPath, fm) {
172
+ const base = path.basename(relPath);
173
+ const slugMatch = base.match(/^(prd|requirements|spec|design-doc|readiness|implementation-plan|ui-spec|scope-check)-(.+)\.md$/);
174
+ const tags = [];
175
+ let featureSlug = fm.feature_slug || fm.feature || '';
176
+ let loadTier = fm.load_tier || 'trigger';
177
+
178
+ if (FOUNDATION_CONTEXT_BASENAMES.has(base)) {
179
+ if (UNIVERSAL_ALWAYS_CONTEXT_BASENAMES.has(base)) {
180
+ loadTier = fm.load_tier || 'always';
181
+ }
182
+ tags.push('foundation');
183
+ }
184
+ if (slugMatch) {
185
+ tags.push(slugMatch[1], 'feature');
186
+ if (!featureSlug) featureSlug = slugMatch[2];
187
+ }
188
+ if (base === 'discovery.md') tags.push('discovery', 'project-memory', 'entities', 'business-rules');
189
+ if (base === 'architecture.md') tags.push('architecture', 'technical-design', 'module-boundary');
190
+ if (base === 'ui-spec.md') tags.push('ui-spec', 'ui', 'ux', 'frontend', 'visual-design');
191
+ if (base === 'scope-check.md') tags.push('scope-check', 'alignment', 'pre-dev');
192
+ if (relPath.includes('/bootstrap/')) tags.push('bootstrap');
193
+ if (relPath.includes('/features/') && base === 'dossier.md') {
194
+ tags.push('feature', 'dossier');
195
+ if (!featureSlug) {
196
+ const parts = relPath.split('/');
197
+ const index = parts.indexOf('features');
198
+ if (index !== -1) featureSlug = parts[index + 1] || '';
199
+ }
200
+ }
201
+
202
+ return { tags, featureSlug, loadTier };
203
+ }
204
+
205
+ async function collectCandidates(targetDir) {
206
+ const candidates = [];
207
+
208
+ for (const surface of SURFACES) {
209
+ const relPaths = await walkMarkdown(targetDir, surface.dir, surface.recursive);
210
+ for (const relPath of relPaths) {
211
+ if (surface.key === 'feature_dossier' && !relPath.endsWith('/dossier.md')) continue;
212
+ const absPath = path.join(targetDir, relPath);
213
+ const content = await readFileSafe(absPath);
214
+ if (!content) continue;
215
+ const stat = await fs.stat(absPath).catch(() => null);
216
+ const fm = parseFrontmatter(content);
217
+ const inferred = inferContextMetadata(relPath, fm);
218
+ const description = fm.description || fm.name || path.basename(relPath, '.md');
219
+ candidates.push({
220
+ path: relPath,
221
+ surface: surface.key,
222
+ size: stat ? stat.size : content.length,
223
+ frontmatter: fm,
224
+ description,
225
+ agents: parseAgentList(fm.agents),
226
+ modes: parseListValue(fm.modes),
227
+ taskTypes: parseListValue(fm.task_types || fm.taskTypes),
228
+ triggers: parseListValue(fm.triggers),
229
+ pathPatterns: parseListValue(fm.paths || fm.globs),
230
+ scope: fm.scope || '',
231
+ featureSlug: fm.feature_slug || fm.feature || inferred.featureSlug || '',
232
+ tags: [...new Set([...parseListValue(fm.tags), ...inferred.tags])],
233
+ loadTier: fm.load_tier || inferred.loadTier || surface.defaultTier
234
+ });
235
+ }
236
+ }
237
+
238
+ return candidates;
239
+ }
240
+
241
+ function keywordMatches(haystack, needles) {
242
+ const normalizedHaystack = normalizeToken(haystack);
243
+ const haystackWords = new Set(normalizedHaystack.split(/\s+/).flatMap(wordVariants));
244
+ return needles.filter((needle) => {
245
+ const normalizedNeedle = normalizeToken(needle);
246
+ if (!normalizedNeedle) return false;
247
+ if (normalizedHaystack.includes(normalizedNeedle)) return true;
248
+ const words = normalizedNeedle.split(/\s+/).filter((word) => word.length >= 4);
249
+ if (words.length === 0) return false;
250
+ const hits = words.filter((word) => wordVariants(word).some((variant) => haystackWords.has(variant))).length;
251
+ return hits >= Math.min(2, words.length);
252
+ });
253
+ }
254
+
255
+ function wordVariants(word) {
256
+ const raw = String(word || '').trim();
257
+ if (!raw) return [];
258
+ const variants = new Set([raw]);
259
+ if (raw.endsWith('ing') && raw.length > 5) {
260
+ const stem = raw.slice(0, -3);
261
+ variants.add(stem);
262
+ variants.add(`${stem}e`);
263
+ }
264
+ if (raw.endsWith('s') && raw.length > 4) variants.add(raw.slice(0, -1));
265
+ return [...variants];
266
+ }
267
+
268
+ function scoreCandidate(candidate, context) {
269
+ const reasons = [];
270
+ let score = 0;
271
+ let effectiveLoadTier = candidate.loadTier;
272
+ const base = path.basename(candidate.path);
273
+
274
+ if (!appliesToAgent(candidate.frontmatter, context.agent)) return null;
275
+ if (context.activationOnly && !ACTIVATION_ONLY_CONTEXT_PATHS.has(candidate.path)) return null;
276
+
277
+ if (candidate.modes.length > 0 && !candidate.modes.map(normalizeToken).includes(context.mode)) {
278
+ return null;
279
+ }
280
+ if (candidate.modes.length > 0) {
281
+ score += 5;
282
+ reasons.push(`mode:${context.mode}`);
283
+ }
284
+
285
+ const agentAlways = AGENT_ALWAYS_CONTEXT_BASENAMES.get(context.agent);
286
+ if (agentAlways && agentAlways.has(base)) {
287
+ effectiveLoadTier = 'always';
288
+ score += 100;
289
+ reasons.push('load_tier:always');
290
+ } else if (candidate.loadTier === 'always') {
291
+ score += 100;
292
+ reasons.push('load_tier:always');
293
+ }
294
+
295
+ const matchedPaths = [];
296
+ for (const requestedPath of context.paths) {
297
+ for (const pattern of candidate.pathPatterns) {
298
+ if (pathMatchesPattern(requestedPath, pattern)) matchedPaths.push(`${requestedPath}~${pattern}`);
299
+ }
300
+ }
301
+ if (matchedPaths.length > 0) {
302
+ score += 10;
303
+ reasons.push(`paths:${matchedPaths.slice(0, 3).join(',')}`);
304
+ }
305
+
306
+ const activeFeature = context.feature || context.activeFeature || '';
307
+ if (context.activationOnly && candidate.featureSlug && !context.feature) {
308
+ return null;
309
+ }
310
+ if (candidate.featureSlug && activeFeature && candidate.featureSlug === activeFeature) {
311
+ score += 45;
312
+ reasons.push(`feature:${candidate.featureSlug}`);
313
+ }
314
+
315
+ if (candidate.featureSlug && context.lookup.includes(normalizeToken(candidate.featureSlug).replace(/-/g, ' '))) {
316
+ score += 45;
317
+ reasons.push(`feature-mentioned:${candidate.featureSlug}`);
318
+ }
319
+
320
+ const matchedTaskTypes = keywordMatches(context.lookup, candidate.taskTypes);
321
+ if (matchedTaskTypes.length > 0) {
322
+ score += 40;
323
+ reasons.push(`task_types:${matchedTaskTypes.slice(0, 3).join(',')}`);
324
+ }
325
+
326
+ const matchedTriggers = keywordMatches(context.lookup, candidate.triggers);
327
+ if (matchedTriggers.length > 0) {
328
+ score += 40;
329
+ reasons.push(`triggers:${matchedTriggers.slice(0, 3).join(',')}`);
330
+ }
331
+
332
+ const matchedTags = keywordMatches(context.lookup, candidate.tags);
333
+ if (matchedTags.length > 0) {
334
+ score += 20;
335
+ reasons.push(`tags:${matchedTags.slice(0, 3).join(',')}`);
336
+ }
337
+
338
+ const descriptionHits = keywordMatches(context.lookup, [
339
+ candidate.description,
340
+ candidate.scope,
341
+ path.basename(candidate.path, '.md').replace(/-/g, ' ')
342
+ ]);
343
+ if (descriptionHits.length > 0) {
344
+ score += 20;
345
+ reasons.push(`description:${descriptionHits.slice(0, 2).join(',')}`);
346
+ }
347
+
348
+ const threshold = effectiveLoadTier === 'justified' ? 50 : 30;
349
+ if (score < threshold) return null;
350
+
351
+ return {
352
+ path: candidate.path,
353
+ surface: candidate.surface,
354
+ load_tier: effectiveLoadTier,
355
+ size: candidate.size,
356
+ score,
357
+ reason: reasons.join('; ')
358
+ };
359
+ }
360
+
361
+ async function selectContext(targetDir, options = {}) {
362
+ const agent = normalizeToken(options.agent || 'dev');
363
+ const mode = modeFromOptions(options.mode);
364
+ const task = String(options.task || options.goal || '').trim();
365
+ const paths = splitOptionList(options.paths || options.path).map(normalizeSlashes);
366
+ const feature = normalizeFeaturePointer(options.feature || options.slug || '');
367
+ const activationOnly = isActivationOnlyTask(agent, mode, task);
368
+
369
+ const pulse = await readProjectPulse(targetDir);
370
+ const devState = await readDevState(targetDir);
371
+ const activeFeature = normalizeFeaturePointer(
372
+ feature || pulse.active_feature || devState.active_feature || ''
373
+ );
374
+
375
+ const lookup = normalizeToken([
376
+ agent,
377
+ mode,
378
+ task,
379
+ paths.join(' '),
380
+ activeFeature
381
+ ].filter(Boolean).join(' '));
382
+
383
+ const candidates = await collectCandidates(targetDir);
384
+ const selected = [];
385
+ for (const candidate of candidates) {
386
+ const scored = scoreCandidate(candidate, {
387
+ agent,
388
+ mode,
389
+ task,
390
+ paths,
391
+ feature,
392
+ activeFeature,
393
+ lookup,
394
+ activationOnly
395
+ });
396
+ if (scored) selected.push(scored);
397
+ }
398
+
399
+ selected.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
400
+
401
+ return {
402
+ ok: true,
403
+ agent,
404
+ mode,
405
+ task,
406
+ paths,
407
+ feature: feature || null,
408
+ active_feature: activeFeature || null,
409
+ activation_only: activationOnly,
410
+ selected
411
+ };
412
+ }
413
+
414
+ module.exports = {
415
+ selectContext,
416
+ collectCandidates,
417
+ parseListValue,
418
+ pathMatchesPattern,
419
+ isActivationOnlyTask
420
+ };
@@ -41,6 +41,20 @@ function findBlockRange(content) {
41
41
  return { start, end };
42
42
  }
43
43
 
44
+ function isLegacyUnmanagedGateway(content) {
45
+ const trimmed = String(content || '').trim();
46
+ if (!trimmed.startsWith('# AIOSON')) return false;
47
+ const hasBoot = trimmed.includes('## Mandatory first action') || trimmed.includes('## Boot');
48
+ const hasProjectContext = trimmed.includes('.aioson/context/project.context.md');
49
+ const hasAgentPointers = trimmed.includes('.aioson/agents/') || trimmed.includes('## Agent files') || trimmed.includes('## Agents');
50
+ const hasManagedMarkers = trimmed.includes(MARKER_BEGIN) || trimmed.includes(MARKER_END);
51
+ return hasBoot && hasProjectContext && hasAgentPointers && !hasManagedMarkers;
52
+ }
53
+
54
+ function stripLegacyUnmanagedGateway(content) {
55
+ return isLegacyUnmanagedGateway(content) ? '' : content;
56
+ }
57
+
44
58
  async function mergeGatewayPointer({ templatePath, targetPath, backupRoot, targetDir, dryRun = false }) {
45
59
  const templateContent = await fs.readFile(templatePath, 'utf8');
46
60
  const block = buildBlock(templateContent);
@@ -56,15 +70,21 @@ async function mergeGatewayPointer({ templatePath, targetPath, backupRoot, targe
56
70
  let next;
57
71
  let action;
58
72
  if (range) {
59
- const before = existing.slice(0, range.start);
73
+ const before = stripLegacyUnmanagedGateway(existing.slice(0, range.start));
60
74
  const after = existing.slice(range.end);
61
75
  const cleanBefore = before.length === 0 || before.endsWith('\n') ? before : `${before}\n`;
62
76
  next = `${cleanBefore}${block}${after}`;
63
77
  action = 'block_updated';
64
78
  } else {
65
- const separator = existing.length === 0 ? '' : existing.endsWith('\n\n') ? '' : existing.endsWith('\n') ? '\n' : '\n\n';
66
- next = `${existing}${separator}${block}`;
67
- action = 'block_appended';
79
+ const cleanExisting = stripLegacyUnmanagedGateway(existing);
80
+ if (cleanExisting.length === 0) {
81
+ next = block;
82
+ action = 'legacy_replaced';
83
+ } else {
84
+ const separator = cleanExisting.length === 0 ? '' : cleanExisting.endsWith('\n\n') ? '' : cleanExisting.endsWith('\n') ? '\n' : '\n\n';
85
+ next = `${cleanExisting}${separator}${block}`;
86
+ action = 'block_appended';
87
+ }
68
88
  }
69
89
 
70
90
  if (next === existing) return { action: 'unchanged' };
@@ -98,5 +118,6 @@ module.exports = {
98
118
  isGatewayPointerPath,
99
119
  buildBlock,
100
120
  findBlockRange,
121
+ isLegacyUnmanagedGateway,
101
122
  mergeGatewayPointer
102
123
  };