@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.
- package/CHANGELOG.md +76 -0
- package/docs/en/1-understand/ecosystem-map.md +45 -29
- package/docs/en/1-understand/glossary.md +6 -6
- package/docs/en/1-understand/what-is-aioson.md +5 -5
- package/docs/en/2-start/existing-project.md +7 -7
- package/docs/en/2-start/first-project.md +33 -39
- package/docs/en/2-start/initial-decisions.md +22 -19
- package/docs/en/3-recipes/README.md +2 -2
- package/docs/en/3-recipes/continuity-between-sessions.md +2 -2
- package/docs/en/3-recipes/from-idea-to-prd-via-briefing.md +13 -3
- package/docs/en/3-recipes/full-feature-with-sheldon.md +329 -338
- package/docs/en/4-agents/README.md +33 -16
- package/docs/en/4-agents/briefing-refiner.md +146 -0
- package/docs/en/4-agents/discovery-design-doc.md +20 -8
- package/docs/en/4-agents/forge-run.md +3 -2
- package/docs/en/5-reference/README.md +1 -0
- package/docs/en/5-reference/autopilot-handoff.md +286 -0
- package/docs/en/5-reference/cli-reference.md +57 -48
- package/docs/en/5-reference/executable-verification.md +10 -7
- package/docs/en/5-reference/parallel.md +2 -0
- package/docs/en/5-reference/qa-browser.md +2 -2
- package/docs/en/README.md +3 -3
- package/docs/pt/1-entender/glossario.md +5 -5
- package/docs/pt/1-entender/mapa-do-ecossistema.md +33 -22
- package/docs/pt/2-comecar/decisoes-iniciais.md +20 -18
- package/docs/pt/2-comecar/primeiro-projeto.md +2 -2
- package/docs/pt/3-receitas/README.md +1 -1
- package/docs/pt/3-receitas/app-saas-do-zero.md +35 -122
- package/docs/pt/3-receitas/da-ideia-ao-prd-via-briefing.md +8 -1
- package/docs/pt/3-receitas/feature-completa-com-sheldon.md +291 -338
- package/docs/pt/4-agentes/README.md +13 -11
- package/docs/pt/4-agentes/analyst.md +9 -5
- package/docs/pt/4-agentes/architect.md +9 -5
- package/docs/pt/4-agentes/briefing-refiner.md +64 -40
- package/docs/pt/4-agentes/briefing.md +6 -1
- package/docs/pt/4-agentes/dev.md +38 -8
- package/docs/pt/4-agentes/deyvin.md +4 -0
- package/docs/pt/4-agentes/discover.md +4 -0
- package/docs/pt/4-agentes/discovery-design-doc.md +25 -23
- package/docs/pt/4-agentes/neo.md +4 -0
- package/docs/pt/4-agentes/orache.md +6 -0
- package/docs/pt/4-agentes/orchestrator.md +176 -118
- package/docs/pt/4-agentes/pentester.md +11 -5
- package/docs/pt/4-agentes/pm.md +15 -7
- package/docs/pt/4-agentes/product.md +19 -1
- package/docs/pt/4-agentes/qa.md +10 -2
- package/docs/pt/4-agentes/setup.md +3 -1
- package/docs/pt/4-agentes/sheldon.md +178 -99
- package/docs/pt/4-agentes/tester.md +6 -0
- package/docs/pt/4-agentes/ux-ui.md +9 -5
- package/docs/pt/5-referencia/README.md +1 -1
- package/docs/pt/5-referencia/agent-chain-continuity.md +14 -5
- package/docs/pt/5-referencia/autopilot-handoff.md +191 -74
- package/docs/pt/5-referencia/comandos-cli.md +16 -7
- package/docs/pt/5-referencia/sdd-automation-scripts.md +6 -6
- package/docs/pt/5-referencia/sdd-framework.md +53 -16
- package/docs/pt/5-referencia/sdd-planos-e-estrutura.md +27 -15
- package/docs/pt/5-referencia/skills.md +2 -0
- package/docs/pt/README.md +4 -4
- package/docs/pt/agentes.md +50 -50
- package/package.json +2 -2
- package/src/agents.js +1 -1
- package/src/artifact-kinds.js +111 -0
- package/src/autopilot-signal.js +71 -0
- package/src/cli.js +112 -81
- package/src/commands/agent-epilogue.js +251 -186
- package/src/commands/agents.js +122 -50
- package/src/commands/audit-code.js +344 -0
- package/src/commands/briefing.js +337 -1
- package/src/commands/classify.js +389 -389
- package/src/commands/feature-close.js +122 -4
- package/src/commands/harness-check.js +259 -175
- package/src/commands/live.js +71 -11
- package/src/commands/review-feature.js +189 -0
- package/src/commands/runtime.js +81 -66
- package/src/commands/sync-agents-copy.js +115 -0
- package/src/commands/update.js +5 -1
- package/src/commands/verification-plan.js +143 -0
- package/src/commands/verify-artifact.js +593 -0
- package/src/commands/workflow-execute.js +434 -316
- package/src/commands/workflow-next.js +193 -20
- package/src/commands/workflow-plan.js +5 -5
- package/src/constants.js +2 -0
- package/src/doctor.js +4 -2
- package/src/gateway-pointer-merge.js +7 -1
- package/src/handoff-contract.js +267 -172
- package/src/harness/contract-integrity-gate.js +172 -0
- package/src/harness/contract-integrity.js +111 -0
- package/src/harness/contract-schema.js +377 -332
- package/src/harness/detect-runtime-feature.js +90 -0
- package/src/harness/static-criteria.js +192 -0
- package/src/i18n/messages/en.js +10 -5
- package/src/i18n/messages/es.js +10 -5
- package/src/i18n/messages/fr.js +10 -5
- package/src/i18n/messages/pt-BR.js +10 -5
- package/src/install-wizard.js +8 -8
- package/src/installer.js +13 -6
- package/src/lib/briefing-refiner/apply-feedback.js +18 -4
- package/src/lib/briefing-refiner/feedback-schema.js +73 -4
- package/src/lib/briefing-refiner/refinement-report.js +11 -0
- package/src/lib/briefing-refiner/review-html.js +388 -68
- package/src/parser.js +12 -0
- package/src/preflight-engine.js +12 -12
- package/src/verification/policy-engine.js +95 -95
- package/src/verification-policy.js +398 -0
- package/src/version.js +52 -1
- package/template/.aioson/agents/architect.md +34 -5
- package/template/.aioson/agents/briefing-refiner.md +91 -48
- package/template/.aioson/agents/briefing.md +4 -0
- package/template/.aioson/agents/committer.md +2 -1
- package/template/.aioson/agents/copywriter.md +30 -21
- package/template/.aioson/agents/design-hybrid-forge.md +10 -1
- package/template/.aioson/agents/dev.md +35 -23
- package/template/.aioson/agents/deyvin.md +4 -0
- package/template/.aioson/agents/discover.md +16 -3
- package/template/.aioson/agents/discovery-design-doc.md +7 -2
- package/template/.aioson/agents/genome.md +19 -10
- package/template/.aioson/agents/neo.md +34 -30
- package/template/.aioson/agents/orache.md +24 -11
- package/template/.aioson/agents/orchestrator.md +100 -7
- package/template/.aioson/agents/pentester.md +4 -0
- package/template/.aioson/agents/pm.md +8 -8
- package/template/.aioson/agents/product.md +25 -1
- package/template/.aioson/agents/profiler-enricher.md +20 -11
- package/template/.aioson/agents/profiler-forge.md +20 -11
- package/template/.aioson/agents/profiler-researcher.md +20 -11
- package/template/.aioson/agents/qa.md +96 -31
- package/template/.aioson/agents/setup.md +12 -1
- package/template/.aioson/agents/sheldon.md +103 -13
- package/template/.aioson/agents/site-forge.md +11 -2
- package/template/.aioson/agents/squad.md +20 -5
- package/template/.aioson/agents/tester.md +4 -0
- package/template/.aioson/agents/ux-ui.md +2 -1
- package/template/.aioson/agents/validator.md +33 -1
- package/template/.aioson/config/verification.json +61 -0
- package/template/.aioson/config.md +13 -9
- package/template/.aioson/docs/LAYERS.md +2 -2
- package/template/.aioson/docs/agent-help.md +126 -0
- package/template/.aioson/docs/autopilot-handoff.md +30 -20
- package/template/.aioson/docs/dev/execution-discipline.md +41 -0
- package/template/.aioson/docs/dev/phase-loop.md +50 -0
- package/template/.aioson/docs/play/llm-data-and-bindings.md +70 -7
- package/template/.aioson/docs/presets/workflow.config.full-merged.json +15 -0
- package/template/.aioson/docs/presets/workflow.config.lean.json +15 -0
- package/template/.aioson/docs/product/prd-contract.md +12 -12
- package/template/.aioson/docs/prototype-contract.md +21 -4
- package/template/.aioson/docs/reference-identity.md +94 -0
- package/template/.aioson/docs/sheldon/harness-contract.md +155 -28
- package/template/.aioson/docs/verification-config.md +82 -0
- package/template/.aioson/docs/verify-artifact-gates.md +91 -0
- package/template/.aioson/docs/workflow-lean-lane.md +129 -0
- package/template/.aioson/skills/design/interface-design/SKILL.md +17 -0
- package/template/.aioson/skills/design/interface-design/references/intent-and-domain.md +2 -0
- package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +16 -14
- package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +2 -2
- package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +4 -4
- package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +15 -15
- package/template/.aioson/skills/process/prototype-forge/SKILL.md +6 -0
- package/template/.aioson/skills/process/reference-identity-extract/SKILL.md +164 -0
- package/template/.aioson/skills/static/multi-agent-patterns.md +5 -5
- package/template/AGENTS.md +36 -36
- 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.
|
|
327
|
-
//
|
|
328
|
-
//
|
|
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 {
|
|
25
|
-
const {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
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 };
|