@jaimevalasek/aioson 1.21.7 → 1.22.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 (105) hide show
  1. package/CHANGELOG.md +39 -2
  2. package/docs/en/1-understand/ecosystem-map.md +1 -1
  3. package/docs/en/2-start/initial-decisions.md +1 -1
  4. package/docs/en/4-agents/README.md +8 -7
  5. package/docs/en/4-agents/discovery-design-doc.md +150 -0
  6. package/docs/en/5-reference/cli-reference.md +42 -16
  7. package/docs/en/README.md +2 -2
  8. package/docs/pt/4-agentes/README.md +8 -6
  9. package/docs/pt/4-agentes/briefing-refiner.md +122 -0
  10. package/docs/pt/4-agentes/discovery-design-doc.md +133 -74
  11. package/docs/pt/4-agentes/scope-check.md +65 -0
  12. package/docs/pt/5-referencia/README.md +1 -0
  13. package/docs/pt/5-referencia/comandos-cli.md +5 -4
  14. package/docs/pt/5-referencia/feature-archive.md +1 -0
  15. package/docs/pt/5-referencia/feature-export.md +155 -0
  16. package/docs/pt/README.md +2 -2
  17. package/docs/pt/agentes.md +3 -1
  18. package/package.json +1 -1
  19. package/src/agent-manifests.js +14 -3
  20. package/src/agents.js +21 -20
  21. package/src/cli.js +72 -52
  22. package/src/commands/briefing.js +28 -150
  23. package/src/commands/commit-prepare.js +5 -2
  24. package/src/commands/feature-archive.js +48 -12
  25. package/src/commands/feature-close.js +40 -0
  26. package/src/commands/feature-export.js +242 -0
  27. package/src/commands/gate-check.js +8 -3
  28. package/src/commands/git-guard.js +58 -0
  29. package/src/commands/harness-gate.js +120 -0
  30. package/src/commands/harness-status.js +157 -0
  31. package/src/commands/harness.js +18 -1
  32. package/src/commands/live.js +120 -115
  33. package/src/commands/parallel-doctor.js +2 -1
  34. package/src/commands/pulse-update.js +2 -2
  35. package/src/commands/scan-project.js +12 -2
  36. package/src/commands/self-implement-loop.js +305 -5
  37. package/src/commands/workflow-next.js +477 -425
  38. package/src/constants.js +21 -11
  39. package/src/context-search.js +3 -0
  40. package/src/doctor.js +24 -8
  41. package/src/dossier/schema.js +4 -3
  42. package/src/harness/active-contract.js +41 -0
  43. package/src/harness/attempt-artifacts.js +95 -0
  44. package/src/harness/budget-guard.js +127 -0
  45. package/src/harness/circuit-breaker.js +7 -0
  46. package/src/harness/contract-schema.js +324 -0
  47. package/src/harness/criteria-runner.js +136 -0
  48. package/src/harness/git-baseline.js +204 -0
  49. package/src/harness/glob-match.js +126 -0
  50. package/src/harness/guard-events.js +71 -0
  51. package/src/harness/human-gate.js +182 -0
  52. package/src/harness/scope-guard.js +115 -0
  53. package/src/i18n/messages/en.js +24 -21
  54. package/src/i18n/messages/es.js +11 -9
  55. package/src/i18n/messages/fr.js +11 -9
  56. package/src/i18n/messages/pt-BR.js +24 -21
  57. package/src/lib/briefing-refiner/apply-feedback.js +134 -0
  58. package/src/lib/briefing-refiner/briefing-paths.js +41 -0
  59. package/src/lib/briefing-refiner/briefing-registry.js +204 -0
  60. package/src/lib/briefing-refiner/briefing-sections.js +110 -0
  61. package/src/lib/briefing-refiner/feedback-schema.js +122 -0
  62. package/src/lib/briefing-refiner/refinement-report.js +39 -0
  63. package/src/lib/briefing-refiner/review-html.js +230 -0
  64. package/src/lib/dev-resume.js +94 -45
  65. package/src/parser.js +8 -5
  66. package/src/preflight-engine.js +88 -84
  67. package/src/runtime-store.js +2 -0
  68. package/src/sandbox.js +17 -3
  69. package/template/.aioson/agents/analyst.md +27 -23
  70. package/template/.aioson/agents/architect.md +7 -3
  71. package/template/.aioson/agents/briefing-refiner.md +121 -0
  72. package/template/.aioson/agents/briefing.md +83 -74
  73. package/template/.aioson/agents/committer.md +8 -0
  74. package/template/.aioson/agents/copywriter.md +19 -7
  75. package/template/.aioson/agents/design-hybrid-forge.md +16 -5
  76. package/template/.aioson/agents/dev.md +68 -66
  77. package/template/.aioson/agents/deyvin.md +97 -90
  78. package/template/.aioson/agents/discover.md +2 -2
  79. package/template/.aioson/agents/discovery-design-doc.md +34 -30
  80. package/template/.aioson/agents/genome.md +82 -71
  81. package/template/.aioson/agents/neo.md +11 -3
  82. package/template/.aioson/agents/orache.md +10 -0
  83. package/template/.aioson/agents/orchestrator.md +68 -68
  84. package/template/.aioson/agents/pentester.md +15 -6
  85. package/template/.aioson/agents/pm.md +30 -25
  86. package/template/.aioson/agents/product.md +108 -108
  87. package/template/.aioson/agents/profiler-enricher.md +10 -0
  88. package/template/.aioson/agents/profiler-forge.md +10 -0
  89. package/template/.aioson/agents/profiler-researcher.md +11 -0
  90. package/template/.aioson/agents/qa.md +28 -20
  91. package/template/.aioson/agents/scope-check.md +176 -164
  92. package/template/.aioson/agents/setup.md +11 -1
  93. package/template/.aioson/agents/sheldon.md +38 -38
  94. package/template/.aioson/agents/site-forge.md +15 -6
  95. package/template/.aioson/agents/squad.md +12 -0
  96. package/template/.aioson/agents/tester.md +209 -209
  97. package/template/.aioson/agents/ux-ui.md +2 -2
  98. package/template/.aioson/agents/validator.md +10 -2
  99. package/template/.aioson/config.md +31 -28
  100. package/template/.aioson/docs/autopilot-handoff.md +46 -0
  101. package/template/.aioson/docs/dossier/agent-templates.md +191 -0
  102. package/template/.aioson/docs/dossier/schema.md +218 -0
  103. package/template/.claude/commands/aioson/agent/briefing-refiner.md +17 -0
  104. package/template/AGENTS.md +50 -47
  105. package/template/CLAUDE.md +29 -27
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Glob matcher mínimo e determinístico para o scope guard (loop-guardrails D1).
5
+ *
6
+ * Subset estrito suportado: `**`, `*`, `?` (incluindo `**` + `/` nas bordas).
7
+ * Qualquer sintaxe fora do subset (extglob `{}[]!()`, classes, negação) é
8
+ * REJEITADA pelo validador — nunca mismatch silencioso em fronteira de
9
+ * segurança. `picomatch` é o upgrade path documentado se o subset apertar.
10
+ *
11
+ * Semântica de caminho (decisão registrada em spec-loop-guardrails.md):
12
+ * - paths e patterns são normalizados para `/` antes do match (EC-6);
13
+ * - pattern SEM `/` casa contra o basename de qualquer profundidade
14
+ * (estilo gitignore: `*.pem` casa `certs/server.pem`);
15
+ * - pattern COM `/` casa contra o caminho relativo completo.
16
+ */
17
+
18
+ const INVALID_GLOB_CHARS = /[{}[\]()!]/;
19
+
20
+ /** Normaliza separadores para `/` e remove `./` inicial. */
21
+ function normalizePath(p) {
22
+ let out = String(p == null ? '' : p).replace(/\\/g, '/');
23
+ while (out.startsWith('./')) out = out.slice(2);
24
+ return out;
25
+ }
26
+
27
+ /**
28
+ * Valida um pattern contra o subset estrito.
29
+ * Retorna { ok: true } ou { ok: false, reason }.
30
+ */
31
+ function validateGlobPattern(pattern) {
32
+ if (typeof pattern !== 'string' || pattern.trim() === '') {
33
+ return { ok: false, reason: 'pattern must be a non-empty string' };
34
+ }
35
+ const normalized = normalizePath(pattern);
36
+ const invalid = normalized.match(INVALID_GLOB_CHARS);
37
+ if (invalid) {
38
+ return {
39
+ ok: false,
40
+ reason: `unsupported glob syntax "${invalid[0]}" — only **, * and ? are allowed (strict subset)`
41
+ };
42
+ }
43
+ return { ok: true };
44
+ }
45
+
46
+ const REGEX_SPECIALS = /[.+^$|]/g;
47
+
48
+ /** Compila um pattern (já validado) para RegExp anchored. */
49
+ function globToRegExp(pattern) {
50
+ const normalized = normalizePath(pattern);
51
+ let regex = '';
52
+ let i = 0;
53
+ while (i < normalized.length) {
54
+ const ch = normalized[i];
55
+ if (ch === '*') {
56
+ if (normalized[i + 1] === '*') {
57
+ // `**` — atravessa separadores
58
+ const prev = normalized[i - 1];
59
+ const next = normalized[i + 2];
60
+ if ((prev === undefined || prev === '/') && next === '/') {
61
+ // `**/` no início ou após `/` — zero ou mais segmentos completos
62
+ regex += '(?:[^/]+/)*';
63
+ i += 3;
64
+ continue;
65
+ }
66
+ if (next === undefined && (prev === undefined || prev === '/')) {
67
+ // `/**` no fim ou pattern `**` puro — qualquer resto (inclusive vazio? não:
68
+ // `secrets/**` exige algo dentro de secrets/; `**` puro casa tudo)
69
+ regex += prev === '/' ? '.+' : '.*';
70
+ i += 2;
71
+ continue;
72
+ }
73
+ // `**` colado em texto (ex.: `a**b`) — trata como `.*`
74
+ regex += '.*';
75
+ i += 2;
76
+ continue;
77
+ }
78
+ regex += '[^/]*';
79
+ i += 1;
80
+ continue;
81
+ }
82
+ if (ch === '?') {
83
+ regex += '[^/]';
84
+ i += 1;
85
+ continue;
86
+ }
87
+ regex += ch.replace(REGEX_SPECIALS, '\\$&');
88
+ i += 1;
89
+ }
90
+ return new RegExp(`^${regex}$`);
91
+ }
92
+
93
+ /**
94
+ * Casa um path contra um pattern do subset.
95
+ * Pattern sem `/` casa contra o basename (estilo gitignore).
96
+ */
97
+ function matchGlob(pattern, filePath) {
98
+ const normalizedPattern = normalizePath(pattern);
99
+ const normalizedPath = normalizePath(filePath);
100
+ if (!normalizedPattern || !normalizedPath) return false;
101
+
102
+ if (!normalizedPattern.includes('/')) {
103
+ const basename = normalizedPath.split('/').pop();
104
+ return globToRegExp(normalizedPattern).test(basename);
105
+ }
106
+ return globToRegExp(normalizedPattern).test(normalizedPath);
107
+ }
108
+
109
+ /**
110
+ * Retorna o primeiro pattern da lista que casa o path, ou null.
111
+ */
112
+ function matchAny(patterns, filePath) {
113
+ if (!Array.isArray(patterns)) return null;
114
+ for (const pattern of patterns) {
115
+ if (matchGlob(pattern, filePath)) return pattern;
116
+ }
117
+ return null;
118
+ }
119
+
120
+ module.exports = {
121
+ normalizePath,
122
+ validateGlobPattern,
123
+ globToRegExp,
124
+ matchGlob,
125
+ matchAny
126
+ };
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Telemetria dos guards do self:loop (loop-guardrails D6).
5
+ *
6
+ * Helper único de emissão para os tipos de evento novos (requirements §2.5):
7
+ * scope_violation, budget_warning, budget_exceeded, runtime_exceeded,
8
+ * human_gate_requested, human_gate_decision, criteria_check_failed,
9
+ * failure_signature_repeat, contract_invalid, diff_limit_exceeded.
10
+ *
11
+ * Sempre best-effort (espelha BR-NC-11 / neural-chain-telemetry): telemetria
12
+ * NUNCA quebra o loop. `token_count` carrega a estimativa chars/4 quando o
13
+ * evento é de tentativa (REQ-7) — telemetria apenas; enforcement lê o
14
+ * acumulador em progress.json (D3).
15
+ */
16
+
17
+ const GUARD_EVENT_TYPES = Object.freeze([
18
+ 'scope_violation',
19
+ 'budget_warning',
20
+ 'budget_exceeded',
21
+ 'runtime_exceeded',
22
+ 'human_gate_requested',
23
+ 'human_gate_decision',
24
+ 'criteria_check_failed',
25
+ 'failure_signature_repeat',
26
+ 'contract_invalid',
27
+ 'diff_limit_exceeded'
28
+ ]);
29
+
30
+ /**
31
+ * Emite um evento de guard no runtime store. Nunca lança.
32
+ *
33
+ * @param {string} targetDir — raiz do projeto
34
+ * @param {object} event
35
+ * @param {string} event.eventType — um de GUARD_EVENT_TYPES
36
+ * @param {string} [event.agent] — default 'self-loop'
37
+ * @param {string} [event.message]
38
+ * @param {object} [event.payload] — vai para payload_json (slug, attempt, etc.)
39
+ * @param {number|null} [event.tokenCount] — estimativa chars/4 da tentativa
40
+ * @returns {boolean} true se gravou
41
+ */
42
+ async function emitGuardEvent(targetDir, { eventType, agent = 'self-loop', message = '', payload = null, tokenCount = null } = {}) {
43
+ if (!GUARD_EVENT_TYPES.includes(eventType)) return false;
44
+ let db = null;
45
+ try {
46
+ const { openRuntimeDb } = require('../runtime-store');
47
+ const opened = await openRuntimeDb(targetDir);
48
+ db = opened.db;
49
+ db.prepare(`
50
+ INSERT INTO execution_events (event_type, agent_name, message, payload_json, token_count, created_at)
51
+ VALUES (?, ?, ?, ?, ?, ?)
52
+ `).run(
53
+ eventType,
54
+ agent,
55
+ message || eventType,
56
+ payload ? JSON.stringify(payload) : null,
57
+ tokenCount === null || tokenCount === undefined ? null : Math.round(tokenCount),
58
+ new Date().toISOString()
59
+ );
60
+ return true;
61
+ } catch {
62
+ return false; // D6: telemetria nunca quebra o loop
63
+ } finally {
64
+ try { if (db) db.close(); } catch { /* ignore */ }
65
+ }
66
+ }
67
+
68
+ module.exports = {
69
+ GUARD_EVENT_TYPES,
70
+ emitGuardEvent
71
+ };
@@ -0,0 +1,182 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Human gates temáticos do self:loop (loop-guardrails REQ-12/13/14/15 + D4).
5
+ *
6
+ * Estado em disco:
7
+ * - `.aioson/plans/{slug}/gates/{id}.json` — decisão humana persistida
8
+ * (schema requirements §2.4 + campo aditivo `run_id` para suprimir
9
+ * re-detecção do mesmo tema dentro do run)
10
+ * - `progress.json` — `status='human_gate'` + `pending_gates[]` (D4)
11
+ *
12
+ * O tema `publish` é gate de COMANDO (intercepta feature:close, REQ-13) —
13
+ * nunca entra na detecção por diff.
14
+ */
15
+
16
+ const fs = require('node:fs');
17
+ const path = require('node:path');
18
+
19
+ const { matchAny } = require('./glob-match');
20
+
21
+ const GATE_STATUSES = Object.freeze(['pending', 'approved', 'rejected']);
22
+
23
+ function gatesDir(planDir) {
24
+ return path.join(planDir, 'gates');
25
+ }
26
+
27
+ function gatePath(planDir, gateId) {
28
+ const safeId = String(gateId).replace(/[^A-Za-z0-9._-]/g, '_');
29
+ return path.join(gatesDir(planDir), `${safeId}.json`);
30
+ }
31
+
32
+ /** Carrega todos os gates do slug (array vazio se nenhum). */
33
+ function loadGates(planDir) {
34
+ const dir = gatesDir(planDir);
35
+ if (!fs.existsSync(dir)) return [];
36
+ const gates = [];
37
+ for (const file of fs.readdirSync(dir)) {
38
+ if (!file.endsWith('.json')) continue;
39
+ try {
40
+ gates.push(JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8')));
41
+ } catch { /* gate corrompido — ignorado na leitura, decisão manual via fs */ }
42
+ }
43
+ return gates;
44
+ }
45
+
46
+ function pendingGates(planDir) {
47
+ return loadGates(planDir).filter((g) => g.status === 'pending');
48
+ }
49
+
50
+ /**
51
+ * Detecção por tema (REQ-12): diff da tentativa casando os globs do tema E
52
+ * tema listado em required_for. `publish` nunca é detectado por diff (REQ-13).
53
+ * Temas já cobertos por gate `approved` do MESMO run não re-disparam.
54
+ *
55
+ * @returns {Array<{theme, triggeredBy: string[]}>}
56
+ */
57
+ function detectGates({ changedFiles = [], requiredFor = [], themePaths = {}, existingGates = [], runId = null }) {
58
+ const detections = [];
59
+ for (const theme of requiredFor) {
60
+ if (theme === 'publish') continue; // gate de comando, nunca diff
61
+ const globs = themePaths[theme] || [];
62
+ if (!globs.length) continue;
63
+ const alreadyHandled = existingGates.some(
64
+ (g) => g.theme === theme && g.run_id === runId && (g.status === 'approved' || g.status === 'pending')
65
+ );
66
+ if (alreadyHandled) continue;
67
+ const triggeredBy = changedFiles
68
+ .filter((f) => matchAny(globs, f.path))
69
+ .map((f) => f.path);
70
+ if (triggeredBy.length > 0) {
71
+ detections.push({ theme, triggeredBy });
72
+ }
73
+ }
74
+ return detections;
75
+ }
76
+
77
+ /**
78
+ * Cria e persiste um gate `pending` (schema §2.4). `id` único por slug:
79
+ * `{theme}-{n}` com n incremental sobre os gates existentes do tema.
80
+ */
81
+ function createGate(planDir, { theme, attempt, triggeredBy = [], diffSummary = '', runId = null }) {
82
+ const existing = loadGates(planDir).filter((g) => g.theme === theme);
83
+ const id = `${theme}-${existing.length + 1}`;
84
+ const gate = {
85
+ id,
86
+ theme,
87
+ status: 'pending',
88
+ attempt,
89
+ triggered_by: triggeredBy,
90
+ diff_summary: diffSummary,
91
+ requested_at: new Date().toISOString(),
92
+ decided_at: null,
93
+ decided_by: null,
94
+ reason: null,
95
+ run_id: runId
96
+ };
97
+ fs.mkdirSync(gatesDir(planDir), { recursive: true });
98
+ fs.writeFileSync(gatePath(planDir, id), JSON.stringify(gate, null, 2), 'utf8');
99
+ return gate;
100
+ }
101
+
102
+ /**
103
+ * Entra no estado HUMAN_GATE (D4): muta `progress` (caller persiste via cb).
104
+ */
105
+ function enterHumanGate(progress, gateIds) {
106
+ progress.status = 'human_gate';
107
+ const pending = new Set(Array.isArray(progress.pending_gates) ? progress.pending_gates : []);
108
+ for (const id of gateIds) pending.add(id);
109
+ progress.pending_gates = [...pending];
110
+ progress.last_updated = new Date().toISOString();
111
+ return progress;
112
+ }
113
+
114
+ /**
115
+ * Decide um gate (REQ-14). Idempotente: gate já decidido → no-op com aviso.
116
+ * EC-8: gate inexistente → erro explícito sem efeito colateral.
117
+ *
118
+ * @returns {{ ok, error?, idempotent?, gate? }}
119
+ */
120
+ function decideGate(planDir, gateId, { decision, by = null, reason = null }) {
121
+ if (!GATE_STATUSES.includes(decision) || decision === 'pending') {
122
+ return { ok: false, error: 'invalid_decision' };
123
+ }
124
+ const file = gatePath(planDir, gateId);
125
+ if (!fs.existsSync(file)) {
126
+ return { ok: false, error: 'gate_not_found', gateId };
127
+ }
128
+ let gate;
129
+ try {
130
+ gate = JSON.parse(fs.readFileSync(file, 'utf8'));
131
+ } catch {
132
+ return { ok: false, error: 'gate_corrupted', gateId };
133
+ }
134
+ if (gate.status !== 'pending') {
135
+ return { ok: true, idempotent: true, gate };
136
+ }
137
+ if (decision === 'rejected' && !(reason && String(reason).trim())) {
138
+ return { ok: false, error: 'reason_required_on_reject', gateId };
139
+ }
140
+ gate.status = decision;
141
+ gate.decided_at = new Date().toISOString();
142
+ gate.decided_by = by || null;
143
+ gate.reason = reason || null;
144
+ fs.writeFileSync(file, JSON.stringify(gate, null, 2), 'utf8');
145
+ return { ok: true, idempotent: false, gate };
146
+ }
147
+
148
+ /**
149
+ * Reconcilia `progress` após decisões (REQ-15): remove o gate decidido de
150
+ * `pending_gates`; sem pendências → `status='in_progress'` (retomada
151
+ * idempotente; gate rejeitado fica como auditoria e não bloqueia runs novos).
152
+ * Muta `progress` (caller persiste).
153
+ */
154
+ function resolveGateState(progress, planDir) {
155
+ const stillPending = new Set(pendingGates(planDir).map((g) => g.id));
156
+ const current = Array.isArray(progress.pending_gates) ? progress.pending_gates : [];
157
+ progress.pending_gates = current.filter((id) => stillPending.has(id));
158
+ if (progress.pending_gates.length === 0 && progress.status === 'human_gate') {
159
+ progress.status = 'in_progress';
160
+ }
161
+ progress.last_updated = new Date().toISOString();
162
+ return progress;
163
+ }
164
+
165
+ /**
166
+ * Gate de comando `publish` (REQ-13): existe gate publish aprovado?
167
+ */
168
+ function hasApprovedPublishGate(planDir) {
169
+ return loadGates(planDir).some((g) => g.theme === 'publish' && g.status === 'approved');
170
+ }
171
+
172
+ module.exports = {
173
+ loadGates,
174
+ pendingGates,
175
+ detectGates,
176
+ createGate,
177
+ enterHumanGate,
178
+ decideGate,
179
+ resolveGateState,
180
+ hasApprovedPublishGate,
181
+ gatePath
182
+ };
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Scope guard do self:loop (loop-guardrails REQ-4/5/6 + REQ-10).
5
+ *
6
+ * Módulo puro: recebe o changed set (já calculado por git-baseline) e o
7
+ * contrato RESOLVIDO (contract-schema.resolveContract — defaults proibidos já
8
+ * mesclados). Não faz I/O.
9
+ *
10
+ * Precedência (REQ-5): deny vence allow — path que casa `forbidden_files` é
11
+ * violação mesmo casando `allowed_files`. Defaults proibidos são sempre
12
+ * aplicados (REQ-4) porque vêm mesclados do resolveContract.
13
+ */
14
+
15
+ const { matchAny } = require('./glob-match');
16
+
17
+ /**
18
+ * @param {object} params
19
+ * @param {Array<{path, status}>} params.changedFiles — changed set da tentativa
20
+ * @param {Array<{path, reason}>} [params.rehashViolations] — D2 (git-baseline)
21
+ * @param {string[]|null} params.allowedGlobs — null = sem allowlist
22
+ * @param {string[]} params.forbiddenGlobs — já mesclados com defaults
23
+ * @returns {{ ok: boolean, violations: Array<{path, status, glob, reason}> }}
24
+ */
25
+ function checkScope({ changedFiles = [], rehashViolations = [], allowedGlobs = null, forbiddenGlobs = [] }) {
26
+ const violations = [];
27
+
28
+ for (const file of changedFiles) {
29
+ const forbiddenMatch = matchAny(forbiddenGlobs, file.path);
30
+ if (forbiddenMatch) {
31
+ violations.push({
32
+ path: file.path,
33
+ status: file.status,
34
+ glob: forbiddenMatch,
35
+ reason: `matches forbidden glob "${forbiddenMatch}"${file.status === 'deleted' ? ' (deletion counts — EC-4)' : ''}`
36
+ });
37
+ continue; // deny vence allow (REQ-5)
38
+ }
39
+ if (allowedGlobs && !matchAny(allowedGlobs, file.path)) {
40
+ violations.push({
41
+ path: file.path,
42
+ status: file.status,
43
+ glob: null,
44
+ reason: 'outside allowed_files allowlist'
45
+ });
46
+ }
47
+ }
48
+
49
+ for (const rehash of rehashViolations) {
50
+ violations.push({
51
+ path: rehash.path,
52
+ status: 'modified',
53
+ glob: null,
54
+ reason: rehash.reason
55
+ });
56
+ }
57
+
58
+ return { ok: violations.length === 0, violations };
59
+ }
60
+
61
+ /** Conta linhas efetivas de diff (+/− excluindo headers +++/---). */
62
+ function countDiffLines(diffPatch) {
63
+ if (!diffPatch) return 0;
64
+ let count = 0;
65
+ for (const line of String(diffPatch).split('\n')) {
66
+ if ((line.startsWith('+') && !line.startsWith('+++')) ||
67
+ (line.startsWith('-') && !line.startsWith('---'))) {
68
+ count += 1;
69
+ }
70
+ }
71
+ return count;
72
+ }
73
+
74
+ /**
75
+ * Limites de diff (REQ-10, should-have). Avaliados sobre o MESMO conjunto do
76
+ * scope guard. `null`/`undefined` = sem limite.
77
+ *
78
+ * @returns {{ ok: boolean, exceeded: Array<{limit, actual, max}> }}
79
+ */
80
+ function checkDiffLimits({ changedFiles = [], diffPatch = '', maxChangedFiles = null, maxDiffLines = null }) {
81
+ const exceeded = [];
82
+
83
+ if (Number.isInteger(maxChangedFiles) && maxChangedFiles > 0 && changedFiles.length > maxChangedFiles) {
84
+ exceeded.push({ limit: 'max_changed_files', actual: changedFiles.length, max: maxChangedFiles });
85
+ }
86
+
87
+ if (Number.isInteger(maxDiffLines) && maxDiffLines > 0) {
88
+ const actual = countDiffLines(diffPatch);
89
+ if (actual > maxDiffLines) {
90
+ exceeded.push({ limit: 'max_diff_lines', actual, max: maxDiffLines });
91
+ }
92
+ }
93
+
94
+ return { ok: exceeded.length === 0, exceeded };
95
+ }
96
+
97
+ /**
98
+ * Feedback de reparo/rollback injetado na próxima iteração após violação
99
+ * (REQ-6). Texto direto para o agente: reverter os paths e permanecer no escopo.
100
+ */
101
+ function buildRollbackFeedback(violations) {
102
+ const lines = violations.slice(0, 10).map((v) => ` - ${v.path} (${v.reason})`);
103
+ return [
104
+ 'SCOPE VIOLATION — files were changed outside the contract scope:',
105
+ ...lines,
106
+ 'Revert these changes (git checkout -- <path> / delete untracked files) and redo the task touching ONLY files inside the allowed scope.'
107
+ ].join('\n');
108
+ }
109
+
110
+ module.exports = {
111
+ checkScope,
112
+ checkDiffLimits,
113
+ countDiffLines,
114
+ buildRollbackFeedback
115
+ };
@@ -182,14 +182,14 @@ module.exports = {
182
182
  'aioson plan [path] [--sub=show|status|checkpoint|stale|register] [--feature=<slug>] [--phase=<N>] [--locale=en]',
183
183
  help_squad_plan:
184
184
  'aioson squad:plan [path] [--sub=show|status|checkpoint|stale|register] [--squad=<slug>] [--round=<N>] [--locale=en]',
185
- help_squad_learning:
186
- 'aioson squad:learning [path] [--sub=list|stats|archive|promote|export] [--squad=<slug>] [--status=<status>] [--locale=en]',
187
- help_agent_audit:
188
- 'aioson agent:audit [path] [--runtime-only|--template-only|--inception] [--locales] [--verbose] [--fix] [--json] [--locale=en]',
189
- help_quality_audit:
190
- 'aioson quality:audit [path] [--feature=<slug>] [--provider-output=<path>] [--baseline=<path>] [--changed=<path[,path]>] [--json] [--locale=en]',
191
- help_squad_dashboard:
192
- 'aioson squad:dashboard [path] [--port=4180] [--squad=<slug>] [--locale=en]',
185
+ help_squad_learning:
186
+ 'aioson squad:learning [path] [--sub=list|stats|archive|promote|export] [--squad=<slug>] [--status=<status>] [--locale=en]',
187
+ help_agent_audit:
188
+ 'aioson agent:audit [path] [--runtime-only|--template-only|--inception] [--locales] [--verbose] [--fix] [--json] [--locale=en]',
189
+ help_quality_audit:
190
+ 'aioson quality:audit [path] [--feature=<slug>] [--provider-output=<path>] [--baseline=<path>] [--changed=<path[,path]>] [--json] [--locale=en]',
191
+ help_squad_dashboard:
192
+ 'aioson squad:dashboard [path] [--port=4180] [--squad=<slug>] [--locale=en]',
193
193
  help_squad_worker:
194
194
  'aioson squad:worker [path] [--sub=list|run|test|logs|scaffold] [--squad=<slug>] [--worker=<slug>] [--input=<json>] [--locale=en]',
195
195
  help_squad_daemon:
@@ -203,7 +203,7 @@ module.exports = {
203
203
  help_commit_prepare:
204
204
  'aioson commit:prepare [path] [--staged-only] [--agent-safe] [--mode=guarded|trusted|headless] [--json] [--locale=en]',
205
205
  help_learning:
206
- 'aioson learning [path] [--sub=list|stats|promote|import-from-claude] [--status=<status>] [--id=<learning-id>] [--project-hash=<hash>] [--dry-run] [--select=<n[,n]|all>] [--locale=en]',
206
+ 'aioson learning [path] [--sub=list|stats|promote|import-from-claude] [--status=<status>] [--id=<learning-id>] [--project-hash=<hash>] [--dry-run] [--select=<n[,n]|all>] [--locale=en]',
207
207
  help_runtime_init:
208
208
  'aioson runtime:init [path] [--json] [--locale=en]',
209
209
  help_runtime_ingest:
@@ -222,12 +222,12 @@ module.exports = {
222
222
  'aioson runtime:task:fail [path] --task=<key> [--goal=<text>] [--json] [--locale=en]',
223
223
  help_runtime_fail:
224
224
  'aioson runtime:fail [path] --run=<key> [--message=<text>] [--summary=<text>] [--output=<path>] [--json] [--locale=en]',
225
- help_runtime_status:
226
- 'aioson runtime:status [path] [--json] [--locale=en]',
227
- help_agent_recover:
228
- 'aioson agent:recover [path] [--dry-run] [--older-than=<24h|7d>] [--json] [--locale=en]',
229
- help_runtime_log:
230
- 'aioson runtime:log [path] --agent=<name> --message=<text> [--type=<event>] [--finish] [--status=completed|failed] [--summary=<text>] [--title=<task-title>] [--json] [--locale=en]',
225
+ help_runtime_status:
226
+ 'aioson runtime:status [path] [--json] [--locale=en]',
227
+ help_agent_recover:
228
+ 'aioson agent:recover [path] [--dry-run] [--older-than=<24h|7d>] [--json] [--locale=en]',
229
+ help_runtime_log:
230
+ 'aioson runtime:log [path] --agent=<name> --message=<text> [--type=<event>] [--finish] [--status=completed|failed] [--summary=<text>] [--title=<task-title>] [--json] [--locale=en]',
231
231
  help_runtime_session_start:
232
232
  'aioson runtime:session:start [path] --agent=<name> [--title=<text>] [--message=<text>] [--session=<key>] [--json] [--locale=en]',
233
233
  help_runtime_session_log:
@@ -258,10 +258,10 @@ module.exports = {
258
258
  'aioson skill:install [path] --slug=<name> [--from=npm|cloud|./path] [--force] [--json] [--locale=en]',
259
259
  help_skill_list:
260
260
  'aioson skill:list [path] [--json] [--locale=en]',
261
- help_skill_remove:
262
- 'aioson skill:remove [path] --slug=<name> [--json] [--locale=en]',
263
- help_skill_audit:
264
- 'aioson skill:audit [path] [--json] [--locale=en]',
261
+ help_skill_remove:
262
+ 'aioson skill:remove [path] --slug=<name> [--json] [--locale=en]',
263
+ help_skill_audit:
264
+ 'aioson skill:audit [path] [--json] [--locale=en]',
265
265
  help_design_hybrid_options:
266
266
  'aioson design-hybrid:options [path] [--advanced] [--json] [--locale=en]',
267
267
  help_cloud_import_squad:
@@ -450,6 +450,8 @@ module.exports = {
450
450
  bootstrap_coverage_hint_seed: 'Run /discover to seed .aioson/context/bootstrap/{what-is,how-it-works,what-it-does,current-state}.md',
451
451
  features_dir_present: 'Features directory present (.aioson/context/features/)',
452
452
  features_dir_present_hint: 'Create .aioson/context/features/ to host per-feature dossiers (doctor --fix will create it).',
453
+ auto_handoff_declared: 'Autopilot handoff flag declared (auto_handoff in project.context.md)',
454
+ auto_handoff_declared_hint: 'The autopilot-handoff protocol is installed but auto_handoff is not set in project.context.md frontmatter — autopilot stays inactive. Set auto_handoff: true to enable it, or auto_handoff: false to silence this warning.',
453
455
  claude_commands_present: 'Claude slash commands present ({missing} missing of {required})',
454
456
  claude_commands_present_hint: 'Missing: {paths}. Run `aioson doctor . --fix` to restore them from the template.',
455
457
  version_drift: 'CLI version matches project.context.md (context: {context}, CLI: {cli})',
@@ -735,7 +737,7 @@ module.exports = {
735
737
  'dApp context detected; include Web3 skills during @architect and @dev.',
736
738
  note_micro_scope: 'Keep implementation scope minimal and avoid optional agents.',
737
739
  note_product_optional: '@product is optional for MICRO — skip it and go straight to @dev if the idea is already clear.',
738
- note_feature_flow: 'New feature workflow (after initial setup): @product → @analyst → @scope-check → @dev → @qa. No @setup required.'
740
+ note_feature_flow: 'New feature workflow (after initial setup): @product → @analyst → @scope-check → @dev → @qa. No @setup required.'
739
741
  },
740
742
  workflow_next: {
741
743
  title: 'Workflow handoff for {mode} ({classification}):',
@@ -1135,6 +1137,7 @@ module.exports = {
1135
1137
  session_already_active: 'Live session already active: {agent} | session: {session} | run: {runKey} ({dbPath})',
1136
1138
  session_started: 'Live session started: {agent} | tool: {tool} | session: {session} ({dbPath})',
1137
1139
  event_recorded: 'Live event recorded: {agent} | {eventType} | {session} ({dbPath})',
1140
+ standalone_event_recorded: 'Standalone runtime event recorded: {agent} | {eventType} | run: {runKey} ({dbPath})',
1138
1141
  handoff_recorded: 'Live handoff recorded: {from} -> {to} | {session} ({dbPath})',
1139
1142
  session_closed: 'Live session closed: {agent} | {session} ({dbPath})',
1140
1143
  process_dead_warning: 'Process is dead while the live session is still open. Close it manually with `aioson live:close . --status=failed`.',
@@ -1209,7 +1212,7 @@ module.exports = {
1209
1212
  folder_required_example_prompt:
1210
1213
  ' Ready prompt : aioson agent:prompt analyst --tool=codex',
1211
1214
  folder_required_example_next:
1212
- ' Workflow after full scan: @analyst -> @scope-check -> @architect -> @dev',
1215
+ ' Workflow after full scan: @analyst -> @scope-check -> @architect -> @dev',
1213
1216
  folder_not_found: 'Folder "{folder}" was not found in this project. Top-level directories detected: {available}',
1214
1217
  config_missing: '{file} not found. To use LLM mode, copy aioson-models.json and fill in your API keys.',
1215
1218
  config_invalid: 'Invalid JSON in aioson-models.json: {error}',
@@ -160,12 +160,12 @@ module.exports = {
160
160
  'aioson squad:pipeline [path] [--sub=list|show|status] [--pipeline=<slug>] [--locale=es]',
161
161
  help_squad_investigate:
162
162
  'aioson squad:investigate [path] [--sub=list|show|score|link|register] [--investigation=<slug>] [--squad=<slug>] [--locale=es]',
163
- help_squad_learning:
164
- 'aioson squad:learning [path] [--sub=list|stats|archive|promote|export] [--squad=<slug>] [--status=<status>] [--locale=es]',
165
- help_quality_audit:
166
- 'aioson quality:audit [path] [--feature=<slug>] [--provider-output=<path>] [--baseline=<path>] [--changed=<path[,path]>] [--json] [--locale=es]',
167
- help_squad_dashboard:
168
- 'aioson squad:dashboard [path] [--port=4180] [--squad=<slug>] [--locale=es]',
163
+ help_squad_learning:
164
+ 'aioson squad:learning [path] [--sub=list|stats|archive|promote|export] [--squad=<slug>] [--status=<status>] [--locale=es]',
165
+ help_quality_audit:
166
+ 'aioson quality:audit [path] [--feature=<slug>] [--provider-output=<path>] [--baseline=<path>] [--changed=<path[,path]>] [--json] [--locale=es]',
167
+ help_squad_dashboard:
168
+ 'aioson squad:dashboard [path] [--port=4180] [--squad=<slug>] [--locale=es]',
169
169
  help_squad_worker:
170
170
  'aioson squad:worker [path] [--sub=list|run|test|logs|scaffold] [--squad=<slug>] [--worker=<slug>] [--input=<json>] [--locale=es]',
171
171
  help_squad_daemon:
@@ -179,7 +179,7 @@ module.exports = {
179
179
  help_commit_prepare:
180
180
  'aioson commit:prepare [path] [--staged-only] [--agent-safe] [--mode=guarded|trusted|headless] [--json] [--locale=es]',
181
181
  help_learning:
182
- 'aioson learning [path] [--sub=list|stats|promote|import-from-claude] [--status=<status>] [--id=<learning-id>] [--project-hash=<hash>] [--dry-run] [--select=<n[,n]|all>] [--locale=es]',
182
+ 'aioson learning [path] [--sub=list|stats|promote|import-from-claude] [--status=<status>] [--id=<learning-id>] [--project-hash=<hash>] [--dry-run] [--select=<n[,n]|all>] [--locale=es]',
183
183
  dashboard_moved:
184
184
  'El flujo `{command}` fue eliminado del CLI. El dashboard de AIOSON ahora se instala por separado. Abre la app del dashboard en tu computadora, crea o agrega un proyecto y selecciona la carpeta que ya contiene `.aioson/`.',
185
185
  dashboard_moved_line: '{message}\n',
@@ -324,6 +324,8 @@ module.exports = {
324
324
  bootstrap_coverage_hint_seed: 'Ejecute /discover para sembrar .aioson/context/bootstrap/{what-is,how-it-works,what-it-does,current-state}.md',
325
325
  features_dir_present: 'Directorio de features presente (.aioson/context/features/)',
326
326
  features_dir_present_hint: 'Cree .aioson/context/features/ para hospedar dossiers por feature (doctor --fix lo crea).',
327
+ auto_handoff_declared: 'Flag de autopilot handoff declarada (auto_handoff en project.context.md)',
328
+ auto_handoff_declared_hint: 'El protocolo autopilot-handoff esta instalado pero auto_handoff no esta definido en el frontmatter de project.context.md — el autopilot queda inactivo. Defina auto_handoff: true para activarlo, o auto_handoff: false para silenciar este aviso.',
327
329
  claude_commands_present: 'Slash commands de Claude presentes ({missing} ausentes de {required})',
328
330
  claude_commands_present_hint: 'Ausentes: {paths}. Ejecute `aioson doctor . --fix` para restaurarlos.',
329
331
  version_drift: 'Version del CLI coincide con project.context.md (contexto: {context}, CLI: {cli})',
@@ -609,7 +611,7 @@ module.exports = {
609
611
  note_product_optional:
610
612
  '@product es opcional para MICRO — omitelo y ve directo a @dev si la idea ya esta clara.',
611
613
  note_feature_flow:
612
- 'Flujo para nueva feature (tras la configuracion inicial): @product → @analyst → @scope-check → @dev → @qa. Sin @setup.'
614
+ 'Flujo para nueva feature (tras la configuracion inicial): @product → @analyst → @scope-check → @dev → @qa. Sin @setup.'
613
615
  },
614
616
  parallel_init: {
615
617
  context_missing:
@@ -1020,7 +1022,7 @@ module.exports = {
1020
1022
  folder_required_example_prompt:
1021
1023
  ' Prompt listo : aioson agent:prompt analyst --tool=codex',
1022
1024
  folder_required_example_next:
1023
- ' Flujo tras escaneo completo: @analyst -> @scope-check -> @architect -> @dev',
1025
+ ' Flujo tras escaneo completo: @analyst -> @scope-check -> @architect -> @dev',
1024
1026
  folder_not_found: 'La carpeta "{folder}" no existe en este proyecto. Directorios de nivel superior detectados: {available}',
1025
1027
  config_missing: '{file} no encontrado. Para usar el modo con LLM, copia aioson-models.json y completa tus claves de API.',
1026
1028
  config_invalid: 'JSON invalido en aioson-models.json: {error}',