@jaimevalasek/aioson 1.33.1 → 1.36.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 (125) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/docs/en/1-understand/ecosystem-map.md +45 -29
  3. package/docs/en/1-understand/glossary.md +5 -5
  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 +32 -38
  7. package/docs/en/2-start/initial-decisions.md +18 -17
  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 +5 -2
  11. package/docs/en/3-recipes/full-feature-with-sheldon.md +327 -338
  12. package/docs/en/4-agents/README.md +28 -14
  13. package/docs/en/4-agents/discovery-design-doc.md +20 -8
  14. package/docs/en/4-agents/forge-run.md +3 -2
  15. package/docs/en/5-reference/cli-reference.md +51 -48
  16. package/docs/en/5-reference/executable-verification.md +10 -7
  17. package/docs/en/5-reference/parallel.md +2 -0
  18. package/docs/en/5-reference/qa-browser.md +2 -2
  19. package/docs/en/README.md +3 -3
  20. package/docs/pt/1-entender/glossario.md +4 -4
  21. package/docs/pt/1-entender/mapa-do-ecossistema.md +32 -21
  22. package/docs/pt/2-comecar/decisoes-iniciais.md +16 -16
  23. package/docs/pt/2-comecar/primeiro-projeto.md +1 -1
  24. package/docs/pt/3-receitas/README.md +1 -1
  25. package/docs/pt/3-receitas/app-saas-do-zero.md +35 -122
  26. package/docs/pt/3-receitas/feature-completa-com-sheldon.md +289 -338
  27. package/docs/pt/4-agentes/analyst.md +9 -5
  28. package/docs/pt/4-agentes/architect.md +9 -5
  29. package/docs/pt/4-agentes/dev.md +19 -7
  30. package/docs/pt/4-agentes/discovery-design-doc.md +25 -23
  31. package/docs/pt/4-agentes/orchestrator.md +164 -118
  32. package/docs/pt/4-agentes/pentester.md +5 -5
  33. package/docs/pt/4-agentes/pm.md +15 -7
  34. package/docs/pt/4-agentes/sheldon.md +166 -99
  35. package/docs/pt/4-agentes/ux-ui.md +7 -4
  36. package/docs/pt/5-referencia/agent-chain-continuity.md +14 -5
  37. package/docs/pt/5-referencia/sdd-automation-scripts.md +6 -6
  38. package/docs/pt/5-referencia/sdd-framework.md +53 -16
  39. package/docs/pt/5-referencia/sdd-planos-e-estrutura.md +27 -15
  40. package/docs/pt/README.md +4 -4
  41. package/docs/pt/agentes.md +48 -50
  42. package/package.json +2 -2
  43. package/src/artifact-kinds.js +110 -0
  44. package/src/cli.js +103 -80
  45. package/src/commands/agent-epilogue.js +251 -186
  46. package/src/commands/agents.js +104 -48
  47. package/src/commands/audit-code.js +344 -0
  48. package/src/commands/classify.js +389 -389
  49. package/src/commands/feature-close.js +43 -18
  50. package/src/commands/harness-check.js +259 -175
  51. package/src/commands/live.js +24 -0
  52. package/src/commands/review-feature.js +189 -0
  53. package/src/commands/runtime.js +81 -66
  54. package/src/commands/sync-agents-copy.js +115 -0
  55. package/src/commands/verification-plan.js +116 -0
  56. package/src/commands/verify-artifact.js +530 -0
  57. package/src/commands/workflow-execute.js +309 -309
  58. package/src/commands/workflow-next.js +139 -10
  59. package/src/commands/workflow-plan.js +5 -5
  60. package/src/constants.js +2 -0
  61. package/src/gateway-pointer-merge.js +7 -1
  62. package/src/handoff-contract.js +267 -172
  63. package/src/harness/contract-integrity-gate.js +172 -0
  64. package/src/harness/contract-integrity.js +111 -0
  65. package/src/harness/contract-schema.js +377 -332
  66. package/src/harness/detect-runtime-feature.js +90 -0
  67. package/src/harness/static-criteria.js +192 -0
  68. package/src/i18n/messages/en.js +8 -4
  69. package/src/i18n/messages/es.js +8 -4
  70. package/src/i18n/messages/fr.js +8 -4
  71. package/src/i18n/messages/pt-BR.js +8 -4
  72. package/src/install-wizard.js +8 -8
  73. package/src/installer.js +13 -6
  74. package/src/parser.js +6 -0
  75. package/src/preflight-engine.js +12 -12
  76. package/src/verification/policy-engine.js +95 -95
  77. package/src/verification-policy.js +398 -0
  78. package/src/version.js +52 -1
  79. package/template/.aioson/agents/architect.md +34 -5
  80. package/template/.aioson/agents/briefing-refiner.md +4 -1
  81. package/template/.aioson/agents/committer.md +2 -1
  82. package/template/.aioson/agents/copywriter.md +30 -21
  83. package/template/.aioson/agents/design-hybrid-forge.md +10 -1
  84. package/template/.aioson/agents/dev.md +27 -22
  85. package/template/.aioson/agents/discover.md +12 -3
  86. package/template/.aioson/agents/discovery-design-doc.md +7 -2
  87. package/template/.aioson/agents/genome.md +19 -10
  88. package/template/.aioson/agents/neo.md +30 -30
  89. package/template/.aioson/agents/orache.md +20 -11
  90. package/template/.aioson/agents/orchestrator.md +84 -7
  91. package/template/.aioson/agents/pm.md +8 -8
  92. package/template/.aioson/agents/profiler-enricher.md +20 -11
  93. package/template/.aioson/agents/profiler-forge.md +20 -11
  94. package/template/.aioson/agents/profiler-researcher.md +20 -11
  95. package/template/.aioson/agents/qa.md +91 -30
  96. package/template/.aioson/agents/setup.md +12 -1
  97. package/template/.aioson/agents/sheldon.md +94 -12
  98. package/template/.aioson/agents/site-forge.md +11 -2
  99. package/template/.aioson/agents/squad.md +20 -5
  100. package/template/.aioson/agents/ux-ui.md +1 -0
  101. package/template/.aioson/agents/validator.md +33 -1
  102. package/template/.aioson/config/verification.json +61 -0
  103. package/template/.aioson/config.md +13 -9
  104. package/template/.aioson/docs/LAYERS.md +2 -2
  105. package/template/.aioson/docs/autopilot-handoff.md +10 -10
  106. package/template/.aioson/docs/dev/execution-discipline.md +41 -0
  107. package/template/.aioson/docs/dev/phase-loop.md +47 -0
  108. package/template/.aioson/docs/presets/workflow.config.full-merged.json +15 -0
  109. package/template/.aioson/docs/presets/workflow.config.lean.json +15 -0
  110. package/template/.aioson/docs/product/prd-contract.md +12 -12
  111. package/template/.aioson/docs/prototype-contract.md +21 -4
  112. package/template/.aioson/docs/reference-identity.md +94 -0
  113. package/template/.aioson/docs/sheldon/harness-contract.md +155 -28
  114. package/template/.aioson/docs/verification-config.md +82 -0
  115. package/template/.aioson/docs/verify-artifact-gates.md +91 -0
  116. package/template/.aioson/docs/workflow-lean-lane.md +129 -0
  117. package/template/.aioson/skills/design/interface-design/references/intent-and-domain.md +2 -0
  118. package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +16 -14
  119. package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +2 -2
  120. package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +4 -4
  121. package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +15 -15
  122. package/template/.aioson/skills/process/prototype-forge/SKILL.md +6 -0
  123. package/template/.aioson/skills/process/reference-identity-extract/SKILL.md +164 -0
  124. package/template/.aioson/skills/static/multi-agent-patterns.md +5 -5
  125. package/template/CLAUDE.md +15 -11
@@ -22,10 +22,14 @@ const { emitDossierEvent } = require('../lib/dossier-telemetry');
22
22
  const { appendScoutToFeatureDossier } = require('../dossier/scout-section');
23
23
  const { emitSubTaskEvent } = require('../sub-task-telemetry');
24
24
  const { loadConfig } = require('../sub-task-engine');
25
- const { runDistillation, readFeatureClassification } = require('../learning-loop-engine');
26
- const { openRuntimeDb } = require('../runtime-store');
27
- const { runNotify } = require('./notify');
28
- const { splitCurrentState, buildArchiveContent, parseActiveSlugs } = require('../current-state-trim');
25
+ const { runDistillation, readFeatureClassification } = require('../learning-loop-engine');
26
+ const { openRuntimeDb } = require('../runtime-store');
27
+ const { runNotify } = require('./notify');
28
+ const { splitCurrentState, buildArchiveContent, parseActiveSlugs } = require('../current-state-trim');
29
+ const {
30
+ evaluateContractIntegrityGate,
31
+ formatContractIntegrityGateError
32
+ } = require('../harness/contract-integrity-gate');
29
33
 
30
34
  // P0 agent-loading-contract: a feature closing is the natural cadence to roll
31
35
  // aged-out current-state.md entries into the cold archive. Conservative window
@@ -322,18 +326,40 @@ async function runFeatureClose({ args, options = {}, logger }) {
322
326
  const updates = [];
323
327
 
324
328
  // 0a. Harness Done Gate (AC-HD-11 refined)
325
- // 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).
329
+ // Only enforced on PASS — FAIL means QA already rejected and we want the
330
+ // closure to record that. Runtime features the CLI can detect (prototype or
331
+ // migration/Prisma evidence) must have a valid contract even on MICRO/SMALL;
332
+ // non-runtime features without a contract keep the historical lightweight path.
329
333
  if (verdict === 'PASS') {
330
334
  const planDir = path.join(targetDir, '.aioson', 'plans', slug);
331
- const contractPath = path.join(planDir, 'harness-contract.json');
332
- const progressPath = path.join(planDir, 'progress.json');
333
- const contractContent = await readFileSafe(contractPath);
334
- const progressContent = await readFileSafe(progressPath);
335
-
336
- // REQ-13 (loop-guardrails): tema `publish` é gate de COMANDO — intercepta
335
+ const contractPath = path.join(planDir, 'harness-contract.json');
336
+ const progressPath = path.join(planDir, 'progress.json');
337
+ const contractContent = await readFileSafe(contractPath);
338
+ const progressContent = await readFileSafe(progressPath);
339
+ const force = options.force === true;
340
+
341
+ const integrityGate = await evaluateContractIntegrityGate(targetDir, slug, {
342
+ runChecks: Boolean(contractContent)
343
+ });
344
+ if (!integrityGate.ok) {
345
+ if (!force) {
346
+ const errMsg = formatContractIntegrityGateError(integrityGate);
347
+ if (options.json) {
348
+ return {
349
+ ok: false,
350
+ reason: 'harness_contract_gate_blocked',
351
+ feature: slug,
352
+ error: errMsg,
353
+ errors: integrityGate.errors
354
+ };
355
+ }
356
+ logger.log(errMsg);
357
+ return { ok: false, reason: 'harness_contract_gate_blocked' };
358
+ }
359
+ updates.push('harness contract gate: BYPASSED via --force');
360
+ }
361
+
362
+ // REQ-13 (loop-guardrails): tema `publish` é gate de COMANDO — intercepta
337
363
  // o feature:close quando o contrato ativo o exige e não há gate publish
338
364
  // aprovado. Nunca detectado por diff. `--force` NÃO bypassa: a aprovação
339
365
  // humana é o propósito do gate (decisão registrada no spec da feature).
@@ -372,10 +398,9 @@ async function runFeatureClose({ args, options = {}, logger }) {
372
398
  }
373
399
  } catch { /* contrato ilegível — o done gate abaixo lida com o estado */ }
374
400
  }
375
-
376
- if (contractContent && progressContent) {
377
- const force = options.force === true;
378
- let progress = null;
401
+
402
+ if (contractContent && progressContent) {
403
+ let progress = null;
379
404
  try {
380
405
  progress = JSON.parse(progressContent);
381
406
  } catch (err) {
@@ -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 };
@@ -262,6 +262,24 @@ function spawnExecutable(binaryPath) {
262
262
  return process.platform === 'win32' ? `"${binaryPath}"` : binaryPath;
263
263
  }
264
264
 
265
+ function isWindowsPidAlive(pid) {
266
+ // process.kill(pid, 0) is not authoritative on Windows: it throws EPERM both
267
+ // for "process exists but no access" AND (commonly) for PIDs that are already
268
+ // gone. Probe the real process table so a dead PID reconciles instead of
269
+ // looking 'unknown' forever (which blocks live:start with "session already
270
+ // active"). Returns true/false when known, null when the probe itself failed.
271
+ try {
272
+ const out = require('node:child_process').execFileSync(
273
+ 'tasklist',
274
+ ['/FI', `PID eq ${pid}`, '/NH', '/FO', 'CSV'],
275
+ { encoding: 'utf8', windowsHide: true, timeout: 3000 }
276
+ );
277
+ return out.includes(`"${pid}"`);
278
+ } catch {
279
+ return null;
280
+ }
281
+ }
282
+
265
283
  function detectProcessState(pid) {
266
284
  if (!pid) return 'not_tracked';
267
285
  try {
@@ -271,6 +289,12 @@ function detectProcessState(pid) {
271
289
  if (error && error.code === 'ESRCH') {
272
290
  return 'dead';
273
291
  }
292
+ if (process.platform === 'win32') {
293
+ const alive = isWindowsPidAlive(Number(pid));
294
+ if (alive === true) return 'alive';
295
+ if (alive === false) return 'dead';
296
+ // probe inconclusive → fall through to 'unknown'
297
+ }
274
298
  return 'unknown';
275
299
  }
276
300
  }