@jaimevalasek/aioson 1.33.1 → 1.37.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 (162) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/docs/en/1-understand/ecosystem-map.md +45 -29
  3. package/docs/en/1-understand/glossary.md +6 -6
  4. package/docs/en/1-understand/what-is-aioson.md +5 -5
  5. package/docs/en/2-start/existing-project.md +7 -7
  6. package/docs/en/2-start/first-project.md +33 -39
  7. package/docs/en/2-start/initial-decisions.md +22 -19
  8. package/docs/en/3-recipes/README.md +2 -2
  9. package/docs/en/3-recipes/continuity-between-sessions.md +2 -2
  10. package/docs/en/3-recipes/from-idea-to-prd-via-briefing.md +13 -3
  11. package/docs/en/3-recipes/full-feature-with-sheldon.md +329 -338
  12. package/docs/en/4-agents/README.md +33 -16
  13. package/docs/en/4-agents/briefing-refiner.md +146 -0
  14. package/docs/en/4-agents/discovery-design-doc.md +20 -8
  15. package/docs/en/4-agents/forge-run.md +3 -2
  16. package/docs/en/5-reference/README.md +1 -0
  17. package/docs/en/5-reference/autopilot-handoff.md +286 -0
  18. package/docs/en/5-reference/cli-reference.md +57 -48
  19. package/docs/en/5-reference/executable-verification.md +10 -7
  20. package/docs/en/5-reference/parallel.md +2 -0
  21. package/docs/en/5-reference/qa-browser.md +2 -2
  22. package/docs/en/README.md +3 -3
  23. package/docs/pt/1-entender/glossario.md +5 -5
  24. package/docs/pt/1-entender/mapa-do-ecossistema.md +33 -22
  25. package/docs/pt/2-comecar/decisoes-iniciais.md +20 -18
  26. package/docs/pt/2-comecar/primeiro-projeto.md +2 -2
  27. package/docs/pt/3-receitas/README.md +1 -1
  28. package/docs/pt/3-receitas/app-saas-do-zero.md +35 -122
  29. package/docs/pt/3-receitas/da-ideia-ao-prd-via-briefing.md +8 -1
  30. package/docs/pt/3-receitas/feature-completa-com-sheldon.md +291 -338
  31. package/docs/pt/4-agentes/README.md +13 -11
  32. package/docs/pt/4-agentes/analyst.md +9 -5
  33. package/docs/pt/4-agentes/architect.md +9 -5
  34. package/docs/pt/4-agentes/briefing-refiner.md +64 -40
  35. package/docs/pt/4-agentes/briefing.md +6 -1
  36. package/docs/pt/4-agentes/dev.md +38 -8
  37. package/docs/pt/4-agentes/deyvin.md +4 -0
  38. package/docs/pt/4-agentes/discover.md +4 -0
  39. package/docs/pt/4-agentes/discovery-design-doc.md +25 -23
  40. package/docs/pt/4-agentes/neo.md +4 -0
  41. package/docs/pt/4-agentes/orache.md +6 -0
  42. package/docs/pt/4-agentes/orchestrator.md +176 -118
  43. package/docs/pt/4-agentes/pentester.md +11 -5
  44. package/docs/pt/4-agentes/pm.md +15 -7
  45. package/docs/pt/4-agentes/product.md +19 -1
  46. package/docs/pt/4-agentes/qa.md +10 -2
  47. package/docs/pt/4-agentes/setup.md +3 -1
  48. package/docs/pt/4-agentes/sheldon.md +178 -99
  49. package/docs/pt/4-agentes/tester.md +6 -0
  50. package/docs/pt/4-agentes/ux-ui.md +9 -5
  51. package/docs/pt/5-referencia/README.md +1 -1
  52. package/docs/pt/5-referencia/agent-chain-continuity.md +14 -5
  53. package/docs/pt/5-referencia/autopilot-handoff.md +191 -74
  54. package/docs/pt/5-referencia/comandos-cli.md +16 -7
  55. package/docs/pt/5-referencia/sdd-automation-scripts.md +6 -6
  56. package/docs/pt/5-referencia/sdd-framework.md +53 -16
  57. package/docs/pt/5-referencia/sdd-planos-e-estrutura.md +27 -15
  58. package/docs/pt/5-referencia/skills.md +2 -0
  59. package/docs/pt/README.md +4 -4
  60. package/docs/pt/agentes.md +50 -50
  61. package/package.json +2 -2
  62. package/src/agents.js +1 -1
  63. package/src/artifact-kinds.js +111 -0
  64. package/src/autopilot-signal.js +71 -0
  65. package/src/cli.js +112 -81
  66. package/src/commands/agent-epilogue.js +251 -186
  67. package/src/commands/agents.js +122 -50
  68. package/src/commands/audit-code.js +344 -0
  69. package/src/commands/briefing.js +337 -1
  70. package/src/commands/classify.js +389 -389
  71. package/src/commands/feature-close.js +122 -4
  72. package/src/commands/harness-check.js +259 -175
  73. package/src/commands/live.js +71 -11
  74. package/src/commands/review-feature.js +189 -0
  75. package/src/commands/runtime.js +81 -66
  76. package/src/commands/sync-agents-copy.js +115 -0
  77. package/src/commands/update.js +5 -1
  78. package/src/commands/verification-plan.js +143 -0
  79. package/src/commands/verify-artifact.js +593 -0
  80. package/src/commands/workflow-execute.js +434 -316
  81. package/src/commands/workflow-next.js +193 -20
  82. package/src/commands/workflow-plan.js +5 -5
  83. package/src/constants.js +2 -0
  84. package/src/doctor.js +4 -2
  85. package/src/gateway-pointer-merge.js +7 -1
  86. package/src/handoff-contract.js +267 -172
  87. package/src/harness/contract-integrity-gate.js +172 -0
  88. package/src/harness/contract-integrity.js +111 -0
  89. package/src/harness/contract-schema.js +377 -332
  90. package/src/harness/detect-runtime-feature.js +90 -0
  91. package/src/harness/static-criteria.js +192 -0
  92. package/src/i18n/messages/en.js +10 -5
  93. package/src/i18n/messages/es.js +10 -5
  94. package/src/i18n/messages/fr.js +10 -5
  95. package/src/i18n/messages/pt-BR.js +10 -5
  96. package/src/install-wizard.js +8 -8
  97. package/src/installer.js +13 -6
  98. package/src/lib/briefing-refiner/apply-feedback.js +18 -4
  99. package/src/lib/briefing-refiner/feedback-schema.js +73 -4
  100. package/src/lib/briefing-refiner/refinement-report.js +11 -0
  101. package/src/lib/briefing-refiner/review-html.js +388 -68
  102. package/src/parser.js +12 -0
  103. package/src/preflight-engine.js +12 -12
  104. package/src/verification/policy-engine.js +95 -95
  105. package/src/verification-policy.js +398 -0
  106. package/src/version.js +52 -1
  107. package/template/.aioson/agents/architect.md +34 -5
  108. package/template/.aioson/agents/briefing-refiner.md +91 -48
  109. package/template/.aioson/agents/briefing.md +4 -0
  110. package/template/.aioson/agents/committer.md +2 -1
  111. package/template/.aioson/agents/copywriter.md +30 -21
  112. package/template/.aioson/agents/design-hybrid-forge.md +10 -1
  113. package/template/.aioson/agents/dev.md +35 -23
  114. package/template/.aioson/agents/deyvin.md +4 -0
  115. package/template/.aioson/agents/discover.md +16 -3
  116. package/template/.aioson/agents/discovery-design-doc.md +7 -2
  117. package/template/.aioson/agents/genome.md +19 -10
  118. package/template/.aioson/agents/neo.md +34 -30
  119. package/template/.aioson/agents/orache.md +24 -11
  120. package/template/.aioson/agents/orchestrator.md +100 -7
  121. package/template/.aioson/agents/pentester.md +4 -0
  122. package/template/.aioson/agents/pm.md +8 -8
  123. package/template/.aioson/agents/product.md +25 -1
  124. package/template/.aioson/agents/profiler-enricher.md +20 -11
  125. package/template/.aioson/agents/profiler-forge.md +20 -11
  126. package/template/.aioson/agents/profiler-researcher.md +20 -11
  127. package/template/.aioson/agents/qa.md +96 -31
  128. package/template/.aioson/agents/setup.md +12 -1
  129. package/template/.aioson/agents/sheldon.md +103 -13
  130. package/template/.aioson/agents/site-forge.md +11 -2
  131. package/template/.aioson/agents/squad.md +20 -5
  132. package/template/.aioson/agents/tester.md +4 -0
  133. package/template/.aioson/agents/ux-ui.md +2 -1
  134. package/template/.aioson/agents/validator.md +33 -1
  135. package/template/.aioson/config/verification.json +61 -0
  136. package/template/.aioson/config.md +13 -9
  137. package/template/.aioson/docs/LAYERS.md +2 -2
  138. package/template/.aioson/docs/agent-help.md +126 -0
  139. package/template/.aioson/docs/autopilot-handoff.md +30 -20
  140. package/template/.aioson/docs/dev/execution-discipline.md +41 -0
  141. package/template/.aioson/docs/dev/phase-loop.md +50 -0
  142. package/template/.aioson/docs/play/llm-data-and-bindings.md +70 -7
  143. package/template/.aioson/docs/presets/workflow.config.full-merged.json +15 -0
  144. package/template/.aioson/docs/presets/workflow.config.lean.json +15 -0
  145. package/template/.aioson/docs/product/prd-contract.md +12 -12
  146. package/template/.aioson/docs/prototype-contract.md +21 -4
  147. package/template/.aioson/docs/reference-identity.md +94 -0
  148. package/template/.aioson/docs/sheldon/harness-contract.md +155 -28
  149. package/template/.aioson/docs/verification-config.md +82 -0
  150. package/template/.aioson/docs/verify-artifact-gates.md +91 -0
  151. package/template/.aioson/docs/workflow-lean-lane.md +129 -0
  152. package/template/.aioson/skills/design/interface-design/SKILL.md +17 -0
  153. package/template/.aioson/skills/design/interface-design/references/intent-and-domain.md +2 -0
  154. package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +16 -14
  155. package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +2 -2
  156. package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +4 -4
  157. package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +15 -15
  158. package/template/.aioson/skills/process/prototype-forge/SKILL.md +6 -0
  159. package/template/.aioson/skills/process/reference-identity-extract/SKILL.md +164 -0
  160. package/template/.aioson/skills/static/multi-agent-patterns.md +5 -5
  161. package/template/AGENTS.md +36 -36
  162. package/template/CLAUDE.md +15 -11
@@ -16,6 +16,7 @@ const fs = require('node:fs/promises');
16
16
  const path = require('node:path');
17
17
  const { contextDir, readFileSafe, parseFrontmatter } = require('../preflight-engine');
18
18
  const { runFeatureArchive } = require('./feature-archive');
19
+ const { runStateReset } = require('./state-save');
19
20
  const dossierBootstrap = require('../dossier/dossier-bootstrap');
20
21
  const dossierStore = require('../dossier/store');
21
22
  const { emitDossierEvent } = require('../lib/dossier-telemetry');
@@ -26,6 +27,10 @@ const { runDistillation, readFeatureClassification } = require('../learning-loop
26
27
  const { openRuntimeDb } = require('../runtime-store');
27
28
  const { runNotify } = require('./notify');
28
29
  const { splitCurrentState, buildArchiveContent, parseActiveSlugs } = require('../current-state-trim');
30
+ const {
31
+ evaluateContractIntegrityGate,
32
+ formatContractIntegrityGateError
33
+ } = require('../harness/contract-integrity-gate');
29
34
 
30
35
  // P0 agent-loading-contract: a feature closing is the natural cadence to roll
31
36
  // aged-out current-state.md entries into the cold archive. Conservative window
@@ -298,6 +303,72 @@ async function archiveScoutsForFeature(targetDir, slug) {
298
303
  return result;
299
304
  }
300
305
 
306
+ // retireDevStateForClosedFeature — clear the @dev cold-start pointer when it
307
+ // still points at the feature being closed. @dev reads dev-state.md DIRECTLY on
308
+ // cold start (its session-start protocol "starts on next_step immediately"),
309
+ // bypassing the dev:resume-data guard that already checks features.md status —
310
+ // so a stale pointer to a done feature would make a future @dev session try to
311
+ // "resume" work that is already closed (e.g. next_step: aioson feature:close).
312
+ // Only retires when active_feature matches this slug; a pointer to a DIFFERENT
313
+ // active feature is left untouched. Archived (not blind-deleted) for audit via
314
+ // the existing state:reset --archive path. Idempotent and best-effort.
315
+ async function retireDevStateForClosedFeature(targetDir, ctxDir, slug) {
316
+ const statePath = path.join(ctxDir, 'dev-state.md');
317
+ const content = await readFileSafe(statePath);
318
+ if (!content) return { retired: false, reason: 'no_dev_state' };
319
+ const fm = parseFrontmatter(content);
320
+ const active = String(fm.active_feature || '').trim();
321
+ if (active !== slug) {
322
+ return { retired: false, reason: 'points_to_other', active_feature: active || null };
323
+ }
324
+ const reset = await runStateReset({
325
+ args: [targetDir],
326
+ options: { archive: true, json: true },
327
+ logger: { log() {} }
328
+ });
329
+ return {
330
+ retired: Boolean(reset && reset.removed),
331
+ archived: reset && reset.archived ? reset.archived : null
332
+ };
333
+ }
334
+
335
+ // retireWorkflowStateForClosedFeature — clear the per-feature workflow runtime
336
+ // state when it still references the feature being closed. Both files live in
337
+ // .aioson/context/, are not slug-named, and are excluded from feature:archive —
338
+ // so without this they linger and break the NEXT feature:
339
+ // - workflow.state.json: seedFeatureWorkflowState refuses to seed a new slug
340
+ // while a stale feature state is present ("different_active_feature").
341
+ // - workflow-execute.json: its enabled agentic_policy is the autopilot signal
342
+ // downstream agents read — a stale one would auto-run the next feature.
343
+ // Only clears when the file references THIS slug (feature mode); a project-mode
344
+ // state or a pointer to another feature is left untouched. Best-effort.
345
+ async function retireWorkflowStateForClosedFeature(ctxDir, slug) {
346
+ const retired = [];
347
+ const statePath = path.join(ctxDir, 'workflow.state.json');
348
+ const stateRaw = await readFileSafe(statePath);
349
+ if (stateRaw) {
350
+ try {
351
+ const st = JSON.parse(stateRaw);
352
+ if (st && st.mode === 'feature' && st.featureSlug === slug) {
353
+ await fs.unlink(statePath);
354
+ retired.push('workflow.state.json');
355
+ }
356
+ } catch { /* malformed — leave it for manual inspection */ }
357
+ }
358
+ const execPath = path.join(ctxDir, 'workflow-execute.json');
359
+ const execRaw = await readFileSafe(execPath);
360
+ if (execRaw) {
361
+ try {
362
+ const ex = JSON.parse(execRaw);
363
+ if (ex && ex.feature === slug) {
364
+ await fs.unlink(execPath);
365
+ retired.push('workflow-execute.json');
366
+ }
367
+ } catch { /* malformed — leave it */ }
368
+ }
369
+ return retired;
370
+ }
371
+
301
372
  async function runFeatureClose({ args, options = {}, logger }) {
302
373
  const targetDir = path.resolve(process.cwd(), args[0] || '.');
303
374
  const slug = options.feature ? String(options.feature) : null;
@@ -323,15 +394,37 @@ async function runFeatureClose({ args, options = {}, logger }) {
323
394
 
324
395
  // 0a. Harness Done Gate (AC-HD-11 refined)
325
396
  // Only enforced on PASS — FAIL means QA already rejected and we want the
326
- // closure to record that. Without `harness-contract.json` for the feature,
327
- // behavior is unchanged (zero impact on MICRO/SMALL or any feature that
328
- // never opted into the harness).
397
+ // closure to record that. Runtime features the CLI can detect (prototype or
398
+ // migration/Prisma evidence) must have a valid contract even on MICRO/SMALL;
399
+ // non-runtime features without a contract keep the historical lightweight path.
329
400
  if (verdict === 'PASS') {
330
401
  const planDir = path.join(targetDir, '.aioson', 'plans', slug);
331
402
  const contractPath = path.join(planDir, 'harness-contract.json');
332
403
  const progressPath = path.join(planDir, 'progress.json');
333
404
  const contractContent = await readFileSafe(contractPath);
334
405
  const progressContent = await readFileSafe(progressPath);
406
+ const force = options.force === true;
407
+
408
+ const integrityGate = await evaluateContractIntegrityGate(targetDir, slug, {
409
+ runChecks: Boolean(contractContent)
410
+ });
411
+ if (!integrityGate.ok) {
412
+ if (!force) {
413
+ const errMsg = formatContractIntegrityGateError(integrityGate);
414
+ if (options.json) {
415
+ return {
416
+ ok: false,
417
+ reason: 'harness_contract_gate_blocked',
418
+ feature: slug,
419
+ error: errMsg,
420
+ errors: integrityGate.errors
421
+ };
422
+ }
423
+ logger.log(errMsg);
424
+ return { ok: false, reason: 'harness_contract_gate_blocked' };
425
+ }
426
+ updates.push('harness contract gate: BYPASSED via --force');
427
+ }
335
428
 
336
429
  // REQ-13 (loop-guardrails): tema `publish` é gate de COMANDO — intercepta
337
430
  // o feature:close quando o contrato ativo o exige e não há gate publish
@@ -374,7 +467,6 @@ async function runFeatureClose({ args, options = {}, logger }) {
374
467
  }
375
468
 
376
469
  if (contractContent && progressContent) {
377
- const force = options.force === true;
378
470
  let progress = null;
379
471
  try {
380
472
  progress = JSON.parse(progressContent);
@@ -446,6 +538,32 @@ async function runFeatureClose({ args, options = {}, logger }) {
446
538
  updates.push('features.md: not found (skipped)');
447
539
  }
448
540
 
541
+ // 2.5. Retire the @dev cold-start pointer if it still points at this feature.
542
+ // On PASS the feature is done; a lingering dev-state.md would make a future
543
+ // @dev cold-start try to resume it (it reads the file directly). FAIL leaves
544
+ // it — @dev keeps working the qa_failed feature. Never touches a pointer to a
545
+ // different active feature. Best-effort; never blocks the close.
546
+ if (verdict === 'PASS') {
547
+ try {
548
+ const ds = await retireDevStateForClosedFeature(targetDir, dir, slug);
549
+ if (ds.retired) {
550
+ updates.push(`dev-state.md: retired closed-feature pointer${ds.archived ? ` (archived to ${ds.archived})` : ''}`);
551
+ } else if (ds.reason === 'points_to_other') {
552
+ updates.push(`dev-state.md: left intact (points at ${ds.active_feature}, not ${slug})`);
553
+ }
554
+ } catch (err) {
555
+ updates.push(`dev-state.md: retire hook error (${(err && err.message) || err})`);
556
+ }
557
+ try {
558
+ const wf = await retireWorkflowStateForClosedFeature(dir, slug);
559
+ if (wf.length > 0) {
560
+ updates.push(`workflow state: retired ${wf.join(', ')} (closed-feature runtime state)`);
561
+ }
562
+ } catch (err) {
563
+ updates.push(`workflow state: retire hook error (${(err && err.message) || err})`);
564
+ }
565
+ }
566
+
449
567
  // 3. Update project-pulse.md
450
568
  const pulsePath = path.join(dir, 'project-pulse.md');
451
569
  const pulseUpdated = await updateProjectPulseFile(
@@ -1,175 +1,259 @@
1
- 'use strict';
2
-
3
- /**
4
- * aioson harness:check [path] --slug=<slug> [--criteria=C1,C2] [--timeout=<ms>]
5
- * [--json] — runner standalone e determinístico de criteria[].verification.
6
- *
7
- * Executa os checks executáveis do contrato FORA do self:loop, para consumo
8
- * pelo @validator (verificação binária antes de julgamento LLM), pelo @dev
9
- * (feedback rápido pré-done) ou por CI. Reusa runCriteria/executeInSandbox —
10
- * NÃO cria runner novo.
11
- *
12
- * Read-only sobre progress.json: quem muda estado do circuito continua sendo
13
- * o ciclo harness:validate/apply-validation. Persiste o resultado em
14
- * `last-check-output.json` no plan dir (espelha a convenção
15
- * last-validator-output.json) e emite telemetria criteria_check_failed
16
- * (best-effort, nunca quebra).
17
- */
18
-
19
- const fs = require('node:fs');
20
- const path = require('node:path');
21
-
22
- const { validateContract, resolveContract } = require('../harness/contract-schema');
23
- const { runCriteria, DEFAULT_CHECK_TIMEOUT_MS } = require('../harness/criteria-runner');
24
- const { emitGuardEvent } = require('../harness/guard-events');
25
- const { findActiveContract } = require('../harness/active-contract');
26
-
27
- function resolveSlug(targetDir, options) {
28
- const explicit = String(options.slug || '').trim();
29
- if (explicit) return explicit;
30
- try {
31
- const active = findActiveContract(targetDir);
32
- return active ? active.slug : '';
33
- } catch {
34
- return '';
35
- }
36
- }
37
-
38
- async function runHarnessCheck({ args, options = {}, logger, t }) {
39
- const targetDir = path.resolve(process.cwd(), args?.[0] || '.');
40
- const slug = resolveSlug(targetDir, options);
41
-
42
- if (!slug) {
43
- logger.error(t('errors.missing_slug') || 'Error: --slug is required');
44
- return { ok: false, error: 'missing_slug' };
45
- }
46
-
47
- const planDir = path.join(targetDir, '.aioson', 'plans', slug);
48
- const contractPath = path.join(planDir, 'harness-contract.json');
49
-
50
- if (!fs.existsSync(contractPath)) {
51
- logger.error(t('harness.contract_not_found', { slug }) || `Contract not found for slug: ${slug}`);
52
- return { ok: false, error: 'contract_not_found', slug };
53
- }
54
-
55
- let contract;
56
- try {
57
- contract = JSON.parse(fs.readFileSync(contractPath, 'utf8'));
58
- } catch (err) {
59
- logger.error(`Invalid JSON in contract: ${err.message}`);
60
- return { ok: false, error: 'invalid_json', slug, detail: err.message };
61
- }
62
-
63
- const schemaResult = validateContract(contract);
64
- if (!schemaResult.ok) {
65
- const first = schemaResult.errors[0];
66
- logger.error(`Contract schema invalid: ${first.field} — ${first.reason}`);
67
- await emitGuardEvent(targetDir, {
68
- eventType: 'contract_invalid',
69
- agent: 'harness-check',
70
- message: `${first.field}: ${first.reason}`,
71
- payload: { slug }
72
- });
73
- return { ok: false, error: 'contract_schema_invalid', slug, errors: schemaResult.errors };
74
- }
75
-
76
- const resolved = resolveContract(contract);
77
-
78
- let criteria = resolved.criteria;
79
- if (options.criteria) {
80
- const wanted = new Set(
81
- String(options.criteria).split(',').map((id) => id.trim()).filter(Boolean)
82
- );
83
- criteria = criteria.filter((c) => c && wanted.has(c.id));
84
- const found = new Set(criteria.map((c) => c.id));
85
- const missing = [...wanted].filter((id) => !found.has(id));
86
- if (missing.length) {
87
- logger.error(t('harness.check_unknown_criteria', { ids: missing.join(', ') }) || `Unknown criteria ids: ${missing.join(', ')}`);
88
- return { ok: false, error: 'unknown_criteria', slug, missing };
89
- }
90
- }
91
-
92
- const timeoutMs = Number.isInteger(Number(options.timeout)) && Number(options.timeout) > 0
93
- ? Number(options.timeout)
94
- : DEFAULT_CHECK_TIMEOUT_MS;
95
-
96
- const executable = criteria.filter(
97
- (c) => c && typeof c.verification === 'string' && c.verification.trim()
98
- );
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
- }
111
-
112
- const checks = await runCriteria({ criteria, cwd: targetDir, timeoutMs });
113
- const failed = checks.filter((c) => !c.ok);
114
-
115
- for (const check of failed) {
116
- await emitGuardEvent(targetDir, {
117
- eventType: 'criteria_check_failed',
118
- agent: 'harness-check',
119
- message: `${check.id}: exit ${check.exitCode}${check.timedOut ? ' (timeout)' : ''}`,
120
- payload: { slug, criterion_id: check.id, exit_code: check.exitCode, signature: check.signature }
121
- });
122
- }
123
-
124
- const report = {
125
- ok: failed.length === 0 && strictErrors.length === 0,
126
- slug,
127
- checked_at: new Date().toISOString(),
128
- strict,
129
- criteria_total: criteria.length,
130
- executable_total: executable.length,
131
- passed: checks.length - failed.length,
132
- failed: failed.length,
133
- skipped_no_verification: skipped,
134
- strict_errors: strictErrors,
135
- checks
136
- };
137
-
138
- try {
139
- if (!fs.existsSync(planDir)) fs.mkdirSync(planDir, { recursive: true });
140
- fs.writeFileSync(
141
- path.join(planDir, 'last-check-output.json'),
142
- JSON.stringify(report, null, 2),
143
- 'utf8'
144
- );
145
- } catch (err) {
146
- // Persistência é conveniência, não gate reporta sem falhar o run.
147
- logger.error(`Could not persist last-check-output.json: ${err.message}`);
148
- }
149
-
150
- if (options.json) {
151
- logger.log(JSON.stringify(report, null, 2));
152
- return report;
153
- }
154
-
155
- logger.log(t('harness.check_header', { slug }) || `Harness check — ${slug}`);
156
- for (const error of strictErrors) {
157
- logger.log(` ${error}`);
158
- }
159
- if (executable.length === 0) {
160
- logger.log(t('harness.check_no_executable', { total: criteria.length }) || ` No criteria with verification commands (${criteria.length} criteria total). @validator judges them all.`);
161
- return report;
162
- }
163
- for (const check of checks) {
164
- const mark = check.ok ? '✓' : '';
165
- const extra = check.ok ? '' : ` (exit ${check.exitCode}${check.timedOut ? ', timeout' : ''})`;
166
- logger.log(` ${mark} ${check.id} ${check.command}${extra} [${check.durationMs}ms]`);
167
- }
168
- logger.log(
169
- t('harness.check_summary', { passed: report.passed, executable: executable.length, skipped }) ||
170
- ` Checks: ${report.passed}/${executable.length} passed (${skipped} without verification — judged by @validator)`
171
- );
172
- return report;
173
- }
174
-
175
- module.exports = { runHarnessCheck };
1
+ 'use strict';
2
+
3
+ /**
4
+ * aioson harness:check [path] --slug=<slug> [--criteria=C1,C2] [--timeout=<ms>]
5
+ * [--json] — runner standalone e determinístico de criteria[].verification.
6
+ *
7
+ * Executa os checks executáveis do contrato FORA do self:loop, para consumo
8
+ * pelo @validator (verificação binária antes de julgamento LLM), pelo @dev
9
+ * (feedback rápido pré-done) ou por CI. Reusa runCriteria/executeInSandbox —
10
+ * NÃO cria runner novo.
11
+ *
12
+ * Read-only sobre progress.json: quem muda estado do circuito continua sendo
13
+ * o ciclo harness:validate/apply-validation. Persiste o resultado em
14
+ * `last-check-output.json` no plan dir (espelha a convenção
15
+ * last-validator-output.json) e emite telemetria criteria_check_failed
16
+ * (best-effort, nunca quebra).
17
+ */
18
+
19
+ const fs = require('node:fs');
20
+ const path = require('node:path');
21
+
22
+ const { validateContract, resolveContract } = require('../harness/contract-schema');
23
+ const { runCriteria, DEFAULT_CHECK_TIMEOUT_MS } = require('../harness/criteria-runner');
24
+ const { evaluateStaticCriteria, isStaticCriterion } = require('../harness/static-criteria');
25
+ const { emitGuardEvent } = require('../harness/guard-events');
26
+ const { findActiveContract } = require('../harness/active-contract');
27
+ const { checkContractIntegrity } = require('../harness/contract-integrity');
28
+ const { detectRuntimeFeature, gitChangedFiles } = require('../harness/detect-runtime-feature');
29
+
30
+ function readProgressSignals(planDir) {
31
+ // Same progress fields the contract-integrity gate consults, so standalone
32
+ // `harness:check` detects the identical migration surface (completed_steps +
33
+ // changed_files + touched_files) the workflow finalize / feature:close gate does.
34
+ try {
35
+ const progressPath = path.join(planDir, 'progress.json');
36
+ if (!fs.existsSync(progressPath)) return [];
37
+ const progress = JSON.parse(fs.readFileSync(progressPath, 'utf8'));
38
+ return [progress.completed_steps, progress.changed_files, progress.touched_files]
39
+ .flatMap((value) => (Array.isArray(value) ? value : []));
40
+ } catch {
41
+ return [];
42
+ }
43
+ }
44
+
45
+ function resolveSlug(targetDir, options) {
46
+ const explicit = String(options.slug || '').trim();
47
+ if (explicit) return explicit;
48
+ try {
49
+ const active = findActiveContract(targetDir);
50
+ return active ? active.slug : '';
51
+ } catch {
52
+ return '';
53
+ }
54
+ }
55
+
56
+ async function runHarnessCheck({ args, options = {}, logger, t }) {
57
+ const targetDir = path.resolve(process.cwd(), args?.[0] || '.');
58
+ const slug = resolveSlug(targetDir, options);
59
+
60
+ if (!slug) {
61
+ logger.error(t('errors.missing_slug') || 'Error: --slug is required');
62
+ return { ok: false, error: 'missing_slug' };
63
+ }
64
+
65
+ const planDir = path.join(targetDir, '.aioson', 'plans', slug);
66
+ const contractPath = path.join(planDir, 'harness-contract.json');
67
+
68
+ if (!fs.existsSync(contractPath)) {
69
+ logger.error(t('harness.contract_not_found', { slug }) || `Contract not found for slug: ${slug}`);
70
+ return { ok: false, error: 'contract_not_found', slug };
71
+ }
72
+
73
+ let contract;
74
+ try {
75
+ contract = JSON.parse(fs.readFileSync(contractPath, 'utf8'));
76
+ } catch (err) {
77
+ logger.error(`Invalid JSON in contract: ${err.message}`);
78
+ return { ok: false, error: 'invalid_json', slug, detail: err.message };
79
+ }
80
+
81
+ const schemaResult = validateContract(contract);
82
+ if (!schemaResult.ok) {
83
+ const first = schemaResult.errors[0];
84
+ logger.error(`Contract schema invalid: ${first.field} ${first.reason}`);
85
+ await emitGuardEvent(targetDir, {
86
+ eventType: 'contract_invalid',
87
+ agent: 'harness-check',
88
+ message: `${first.field}: ${first.reason}`,
89
+ payload: { slug }
90
+ });
91
+ return { ok: false, error: 'contract_schema_invalid', slug, errors: schemaResult.errors };
92
+ }
93
+
94
+ const resolved = resolveContract(contract);
95
+
96
+ // ── Contract-integrity precheck (§2c, deterministic core of @validator Step 0) ──
97
+ // A runtime feature whose contract has no RG-* criterion, or that pads binary
98
+ // criteria with duplicate verification commands, cannot prove the app runs.
99
+ // Detection only fires on signals the framework locates reliably (prototype
100
+ // manifest, migration steps); the Play has_api trigger stays with @validator.
101
+ const runtime = detectRuntimeFeature(targetDir, slug, {
102
+ completedSteps: readProgressSignals(planDir),
103
+ changedFiles: gitChangedFiles(targetDir)
104
+ });
105
+ const integrity = checkContractIntegrity(contract, { isRuntimeFeature: runtime.isRuntimeFeature });
106
+ for (const err of integrity.errors) {
107
+ await emitGuardEvent(targetDir, {
108
+ eventType: 'contract_integrity_failed',
109
+ agent: 'harness-check',
110
+ message: `${err.code}: ${err.message}`,
111
+ payload: { slug, code: err.code, signals: runtime.signals }
112
+ });
113
+ }
114
+
115
+ let criteria = resolved.criteria;
116
+ if (options.criteria) {
117
+ const wanted = new Set(
118
+ String(options.criteria).split(',').map((id) => id.trim()).filter(Boolean)
119
+ );
120
+ criteria = criteria.filter((c) => c && wanted.has(c.id));
121
+ const found = new Set(criteria.map((c) => c.id));
122
+ const missing = [...wanted].filter((id) => !found.has(id));
123
+ if (missing.length) {
124
+ logger.error(t('harness.check_unknown_criteria', { ids: missing.join(', ') }) || `Unknown criteria ids: ${missing.join(', ')}`);
125
+ return { ok: false, error: 'unknown_criteria', slug, missing };
126
+ }
127
+ }
128
+
129
+ const timeoutMs = Number.isInteger(Number(options.timeout)) && Number(options.timeout) > 0
130
+ ? Number(options.timeout)
131
+ : DEFAULT_CHECK_TIMEOUT_MS;
132
+
133
+ const executable = criteria.filter(
134
+ (c) => c && typeof c.verification === 'string' && c.verification.trim()
135
+ );
136
+ // Static (SG-*) criteria are deterministically checkable too — they are NOT
137
+ // "skipped / @validator-judged", so exclude them from that count and from the
138
+ // strict-mode verification-debt checks below.
139
+ const staticCriteria = criteria.filter((c) => isStaticCriterion(c));
140
+ const skipped = criteria.length - executable.length - staticCriteria.length;
141
+ const strict = Boolean(options.strict);
142
+ const binaryWithoutVerification = criteria.filter(
143
+ (c) => c && c.binary === true && !(typeof c.verification === 'string' && c.verification.trim()) && !isStaticCriterion(c)
144
+ );
145
+ const strictErrors = [];
146
+ if (strict && criteria.length > 0 && executable.length === 0 && staticCriteria.length === 0) {
147
+ strictErrors.push('strict mode requires at least one executable or static (SG-*) verification criterion');
148
+ }
149
+ if (strict && binaryWithoutVerification.length > 0) {
150
+ strictErrors.push(`strict mode requires verification for binary criteria: ${binaryWithoutVerification.map((c) => c.id).join(', ')}`);
151
+ }
152
+
153
+ const checks = await runCriteria({ criteria, cwd: targetDir, timeoutMs });
154
+ const failed = checks.filter((c) => !c.ok);
155
+
156
+ // SG-* static criteria — build-independent (fs + RegExp + parse), evaluated on
157
+ // every run (cheap). They gate the report just like executable criteria.
158
+ const staticResult = evaluateStaticCriteria({ criteria, cwd: targetDir });
159
+ const staticFailed = staticResult.checks.filter((c) => !c.ok);
160
+
161
+ for (const check of failed) {
162
+ await emitGuardEvent(targetDir, {
163
+ eventType: 'criteria_check_failed',
164
+ agent: 'harness-check',
165
+ message: `${check.id}: exit ${check.exitCode}${check.timedOut ? ' (timeout)' : ''}`,
166
+ payload: { slug, criterion_id: check.id, exit_code: check.exitCode, signature: check.signature }
167
+ });
168
+ }
169
+ for (const check of staticFailed) {
170
+ await emitGuardEvent(targetDir, {
171
+ eventType: 'criteria_check_failed',
172
+ agent: 'harness-check',
173
+ message: `${check.id}: ${check.detail}`,
174
+ payload: { slug, criterion_id: check.id, kind: 'static' }
175
+ });
176
+ }
177
+
178
+ const report = {
179
+ ok: failed.length === 0 && staticFailed.length === 0 && strictErrors.length === 0 && integrity.ok,
180
+ slug,
181
+ checked_at: new Date().toISOString(),
182
+ strict,
183
+ criteria_total: criteria.length,
184
+ executable_total: executable.length,
185
+ passed: checks.length - failed.length,
186
+ failed: failed.length,
187
+ skipped_no_verification: skipped,
188
+ static_total: staticResult.total,
189
+ static_passed: staticResult.total - staticFailed.length,
190
+ static_failed: staticFailed.length,
191
+ strict_errors: strictErrors,
192
+ integrity: {
193
+ ok: integrity.ok,
194
+ is_runtime_feature: runtime.isRuntimeFeature,
195
+ runtime_signals: runtime.signals,
196
+ errors: integrity.errors,
197
+ warnings: integrity.warnings
198
+ },
199
+ checks,
200
+ static_checks: staticResult.checks
201
+ };
202
+
203
+ try {
204
+ if (!fs.existsSync(planDir)) fs.mkdirSync(planDir, { recursive: true });
205
+ fs.writeFileSync(
206
+ path.join(planDir, 'last-check-output.json'),
207
+ JSON.stringify(report, null, 2),
208
+ 'utf8'
209
+ );
210
+ } catch (err) {
211
+ // Persistência é conveniência, não gate — reporta sem falhar o run.
212
+ logger.error(`Could not persist last-check-output.json: ${err.message}`);
213
+ }
214
+
215
+ if (options.json) {
216
+ logger.log(JSON.stringify(report, null, 2));
217
+ return report;
218
+ }
219
+
220
+ logger.log(t('harness.check_header', { slug }) || `Harness check — ${slug}`);
221
+ for (const err of integrity.errors) {
222
+ logger.log(` ✗ contract-integrity (${err.code}): ${err.message}`);
223
+ }
224
+ for (const warn of integrity.warnings) {
225
+ logger.log(` ⚠ contract-integrity (${warn.code}): ${warn.message}`);
226
+ }
227
+ for (const error of strictErrors) {
228
+ logger.log(` ✗ ${error}`);
229
+ }
230
+ // SG-* static criteria render on every run — they are build-independent.
231
+ for (const check of staticResult.checks) {
232
+ const mark = check.ok ? '✓' : '✗';
233
+ const extra = check.ok ? ` [${check.files.join(', ')}]` : ` — ${check.detail}`;
234
+ logger.log(` ${mark} ${check.id} (static)${extra}`);
235
+ }
236
+ if (executable.length === 0) {
237
+ if (staticResult.total > 0) {
238
+ logger.log(` Static: ${report.static_passed}/${staticResult.total} SG-* passed${skipped > 0 ? ` (${skipped} without verification — judged by @validator)` : ''}.`);
239
+ } else {
240
+ logger.log(t('harness.check_no_executable', { total: criteria.length }) || ` No criteria with verification commands (${criteria.length} criteria total). @validator judges them all.`);
241
+ }
242
+ return report;
243
+ }
244
+ for (const check of checks) {
245
+ const mark = check.ok ? '✓' : '✗';
246
+ const extra = check.ok ? '' : ` (exit ${check.exitCode}${check.timedOut ? ', timeout' : ''})`;
247
+ logger.log(` ${mark} ${check.id} — ${check.command}${extra} [${check.durationMs}ms]`);
248
+ }
249
+ logger.log(
250
+ t('harness.check_summary', { passed: report.passed, executable: executable.length, skipped }) ||
251
+ ` Checks: ${report.passed}/${executable.length} passed (${skipped} without verification — judged by @validator)`
252
+ );
253
+ if (staticResult.total > 0) {
254
+ logger.log(` Static: ${report.static_passed}/${staticResult.total} SG-* passed.`);
255
+ }
256
+ return report;
257
+ }
258
+
259
+ module.exports = { runHarnessCheck };