agentera 0.0.0 → 3.0.0-dev.1
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/README.md +6 -45
- package/bundle/.agentera-npx-bundle.json +4 -0
- package/bundle/references/adapters/cursor.md +213 -0
- package/bundle/references/adapters/opencode.md +530 -0
- package/bundle/references/adapters/package-manifest-interface-model.yaml +337 -0
- package/bundle/references/adapters/package-registry.yaml +247 -0
- package/bundle/references/adapters/package-surface-characterization.md +48 -0
- package/bundle/references/adapters/runtime-adapter-characterization.md +79 -0
- package/bundle/references/adapters/runtime-adapter-interface-model.yaml +200 -0
- package/bundle/references/adapters/runtime-adapter-registry.yaml +548 -0
- package/bundle/references/adapters/runtime-feature-parity.md +189 -0
- package/bundle/references/analysis/benchmark.md +267 -0
- package/bundle/references/analysis/startup-measurement-contract.yaml +424 -0
- package/bundle/references/artifacts/artifact-registry-interface-model.yaml +288 -0
- package/bundle/references/cli/agent-ready-state-contract.yaml +950 -0
- package/bundle/references/cli/app-lifecycle-vocabulary.yaml +241 -0
- package/bundle/references/cli/audience-namespace-cli-migration.yaml +355 -0
- package/bundle/references/cli/bundle-skill-vocabulary.yaml +278 -0
- package/bundle/references/cli/capability-instruction-contract.yaml +123 -0
- package/bundle/references/cli/capability-tool-classification.yaml +53 -0
- package/bundle/references/cli/routing-execution-vocabulary.yaml +281 -0
- package/bundle/references/cli/update-channels.yaml +147 -0
- package/bundle/references/cli/vocabulary-index.yaml +160 -0
- package/bundle/references/cli/vocabulary.md +566 -0
- package/bundle/references/meta/documentation-inventory.md +43 -0
- package/bundle/references/v1-section-mapping.md +47 -0
- package/bundle/registry.json +39 -0
- package/bundle/skills/agentera/.claude-plugin/plugin.json +27 -0
- package/bundle/skills/agentera/SKILL.md +470 -0
- package/bundle/skills/agentera/agents/dokumentera.toml +6 -0
- package/bundle/skills/agentera/agents/hej.toml +6 -0
- package/bundle/skills/agentera/agents/inspektera.toml +6 -0
- package/bundle/skills/agentera/agents/inspirera.toml +6 -0
- package/bundle/skills/agentera/agents/optimera.toml +6 -0
- package/bundle/skills/agentera/agents/orkestrera.toml +6 -0
- package/bundle/skills/agentera/agents/planera.toml +6 -0
- package/bundle/skills/agentera/agents/profilera.toml +6 -0
- package/bundle/skills/agentera/agents/realisera.toml +6 -0
- package/bundle/skills/agentera/agents/resonera.toml +6 -0
- package/bundle/skills/agentera/agents/visionera.toml +6 -0
- package/bundle/skills/agentera/agents/visualisera.toml +6 -0
- package/bundle/skills/agentera/capabilities/dokumentera/instructions.md +428 -0
- package/bundle/skills/agentera/capabilities/dokumentera/schemas/artifacts.yaml +73 -0
- package/bundle/skills/agentera/capabilities/dokumentera/schemas/exit.yaml +35 -0
- package/bundle/skills/agentera/capabilities/dokumentera/schemas/triggers.yaml +35 -0
- package/bundle/skills/agentera/capabilities/dokumentera/schemas/validation.yaml +139 -0
- package/bundle/skills/agentera/capabilities/hej/instructions.md +331 -0
- package/bundle/skills/agentera/capabilities/hej/schemas/artifacts.yaml +69 -0
- package/bundle/skills/agentera/capabilities/hej/schemas/exit.yaml +32 -0
- package/bundle/skills/agentera/capabilities/hej/schemas/triggers.yaml +58 -0
- package/bundle/skills/agentera/capabilities/hej/schemas/validation.yaml +55 -0
- package/bundle/skills/agentera/capabilities/inspektera/instructions.md +514 -0
- package/bundle/skills/agentera/capabilities/inspektera/schemas/artifacts.yaml +76 -0
- package/bundle/skills/agentera/capabilities/inspektera/schemas/exit.yaml +36 -0
- package/bundle/skills/agentera/capabilities/inspektera/schemas/triggers.yaml +38 -0
- package/bundle/skills/agentera/capabilities/inspektera/schemas/validation.yaml +113 -0
- package/bundle/skills/agentera/capabilities/inspirera/instructions.md +280 -0
- package/bundle/skills/agentera/capabilities/inspirera/schemas/artifacts.yaml +24 -0
- package/bundle/skills/agentera/capabilities/inspirera/schemas/exit.yaml +33 -0
- package/bundle/skills/agentera/capabilities/inspirera/schemas/triggers.yaml +34 -0
- package/bundle/skills/agentera/capabilities/inspirera/schemas/validation.yaml +58 -0
- package/bundle/skills/agentera/capabilities/optimera/instructions.md +437 -0
- package/bundle/skills/agentera/capabilities/optimera/schemas/artifacts.yaml +69 -0
- package/bundle/skills/agentera/capabilities/optimera/schemas/exit.yaml +35 -0
- package/bundle/skills/agentera/capabilities/optimera/schemas/triggers.yaml +39 -0
- package/bundle/skills/agentera/capabilities/optimera/schemas/validation.yaml +91 -0
- package/bundle/skills/agentera/capabilities/orkestrera/instructions.md +433 -0
- package/bundle/skills/agentera/capabilities/orkestrera/schemas/artifacts.yaml +64 -0
- package/bundle/skills/agentera/capabilities/orkestrera/schemas/exit.yaml +34 -0
- package/bundle/skills/agentera/capabilities/orkestrera/schemas/triggers.yaml +42 -0
- package/bundle/skills/agentera/capabilities/orkestrera/schemas/validation.yaml +107 -0
- package/bundle/skills/agentera/capabilities/planera/instructions.md +368 -0
- package/bundle/skills/agentera/capabilities/planera/schemas/artifacts.yaml +62 -0
- package/bundle/skills/agentera/capabilities/planera/schemas/exit.yaml +33 -0
- package/bundle/skills/agentera/capabilities/planera/schemas/triggers.yaml +34 -0
- package/bundle/skills/agentera/capabilities/planera/schemas/validation.yaml +61 -0
- package/bundle/skills/agentera/capabilities/profilera/instructions.md +419 -0
- package/bundle/skills/agentera/capabilities/profilera/schemas/artifacts.yaml +18 -0
- package/bundle/skills/agentera/capabilities/profilera/schemas/exit.yaml +34 -0
- package/bundle/skills/agentera/capabilities/profilera/schemas/triggers.yaml +45 -0
- package/bundle/skills/agentera/capabilities/profilera/schemas/validation.yaml +57 -0
- package/bundle/skills/agentera/capabilities/realisera/instructions.md +403 -0
- package/bundle/skills/agentera/capabilities/realisera/schemas/artifacts.yaml +80 -0
- package/bundle/skills/agentera/capabilities/realisera/schemas/exit.yaml +35 -0
- package/bundle/skills/agentera/capabilities/realisera/schemas/triggers.yaml +39 -0
- package/bundle/skills/agentera/capabilities/realisera/schemas/validation.yaml +110 -0
- package/bundle/skills/agentera/capabilities/resonera/instructions.md +329 -0
- package/bundle/skills/agentera/capabilities/resonera/schemas/artifacts.yaml +47 -0
- package/bundle/skills/agentera/capabilities/resonera/schemas/exit.yaml +35 -0
- package/bundle/skills/agentera/capabilities/resonera/schemas/triggers.yaml +46 -0
- package/bundle/skills/agentera/capabilities/resonera/schemas/validation.yaml +77 -0
- package/bundle/skills/agentera/capabilities/visionera/instructions.md +309 -0
- package/bundle/skills/agentera/capabilities/visionera/schemas/artifacts.yaml +57 -0
- package/bundle/skills/agentera/capabilities/visionera/schemas/exit.yaml +35 -0
- package/bundle/skills/agentera/capabilities/visionera/schemas/triggers.yaml +41 -0
- package/bundle/skills/agentera/capabilities/visionera/schemas/validation.yaml +74 -0
- package/bundle/skills/agentera/capabilities/visualisera/instructions.md +400 -0
- package/bundle/skills/agentera/capabilities/visualisera/schemas/artifacts.yaml +44 -0
- package/bundle/skills/agentera/capabilities/visualisera/schemas/exit.yaml +34 -0
- package/bundle/skills/agentera/capabilities/visualisera/schemas/triggers.yaml +33 -0
- package/bundle/skills/agentera/capabilities/visualisera/schemas/validation.yaml +80 -0
- package/bundle/skills/agentera/capability_schema_contract.yaml +385 -0
- package/bundle/skills/agentera/protocol.yaml +463 -0
- package/bundle/skills/agentera/references/contract.md +1039 -0
- package/bundle/skills/agentera/schemas/artifacts/changelog.yaml +60 -0
- package/bundle/skills/agentera/schemas/artifacts/decisions.yaml +461 -0
- package/bundle/skills/agentera/schemas/artifacts/design.yaml +55 -0
- package/bundle/skills/agentera/schemas/artifacts/docs.yaml +402 -0
- package/bundle/skills/agentera/schemas/artifacts/experiments.yaml +373 -0
- package/bundle/skills/agentera/schemas/artifacts/health.yaml +484 -0
- package/bundle/skills/agentera/schemas/artifacts/objective.yaml +399 -0
- package/bundle/skills/agentera/schemas/artifacts/plan.yaml +342 -0
- package/bundle/skills/agentera/schemas/artifacts/progress.yaml +325 -0
- package/bundle/skills/agentera/schemas/artifacts/todo.yaml +110 -0
- package/bundle/skills/agentera/schemas/artifacts/vision.yaml +262 -0
- package/bundle/skills/hej/.claude-plugin/plugin.json +6 -0
- package/bundle/skills/hej/SKILL.md +69 -0
- package/bundle/skills/hej/agents/hej.toml +11 -0
- package/bundle/skills/hej/agents/openai.yaml +8 -0
- package/dist/analytics/extractCorpus.js +1791 -0
- package/dist/analytics/extractCorpus.js.map +1 -0
- package/dist/analytics/usageStats.js +487 -0
- package/dist/analytics/usageStats.js.map +1 -0
- package/dist/bin/agentera.js +4 -0
- package/dist/bin/agentera.js.map +1 -0
- package/dist/cli/appContext.js +226 -0
- package/dist/cli/appContext.js.map +1 -0
- package/dist/cli/argvalidate.js +41 -0
- package/dist/cli/argvalidate.js.map +1 -0
- package/dist/cli/capabilityContext.js +2421 -0
- package/dist/cli/capabilityContext.js.map +1 -0
- package/dist/cli/commands/backfill.js +84 -0
- package/dist/cli/commands/backfill.js.map +1 -0
- package/dist/cli/commands/capability.js +44 -0
- package/dist/cli/commands/capability.js.map +1 -0
- package/dist/cli/commands/compact.js +148 -0
- package/dist/cli/commands/compact.js.map +1 -0
- package/dist/cli/commands/doctor.js +180 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/lint.js +179 -0
- package/dist/cli/commands/lint.js.map +1 -0
- package/dist/cli/commands/prime.js +544 -0
- package/dist/cli/commands/prime.js.map +1 -0
- package/dist/cli/commands/query.js +346 -0
- package/dist/cli/commands/query.js.map +1 -0
- package/dist/cli/commands/report.js +210 -0
- package/dist/cli/commands/report.js.map +1 -0
- package/dist/cli/commands/schema.js +306 -0
- package/dist/cli/commands/schema.js.map +1 -0
- package/dist/cli/commands/state.js +1012 -0
- package/dist/cli/commands/state.js.map +1 -0
- package/dist/cli/commands/upgrade.js +48 -0
- package/dist/cli/commands/upgrade.js.map +1 -0
- package/dist/cli/commands/validate.js +519 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/commands/verify.js +204 -0
- package/dist/cli/commands/verify.js.map +1 -0
- package/dist/cli/dispatch.js +958 -0
- package/dist/cli/dispatch.js.map +1 -0
- package/dist/cli/orientation.js +595 -0
- package/dist/cli/orientation.js.map +1 -0
- package/dist/cli/prime-blob.js +3 -0
- package/dist/cli/prime-blob.js.map +1 -0
- package/dist/cli/stateQuery.js +292 -0
- package/dist/cli/stateQuery.js.map +1 -0
- package/dist/cli/structured.js +18 -0
- package/dist/cli/structured.js.map +1 -0
- package/dist/core/difflib.js +274 -0
- package/dist/core/difflib.js.map +1 -0
- package/dist/core/git.js +43 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/paths.js +50 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/pyjson.js +101 -0
- package/dist/core/pyjson.js.map +1 -0
- package/dist/core/sourceRoot.js +72 -0
- package/dist/core/sourceRoot.js.map +1 -0
- package/dist/core/toml.js +11 -0
- package/dist/core/toml.js.map +1 -0
- package/dist/core/yaml.js +25 -0
- package/dist/core/yaml.js.map +1 -0
- package/dist/eval/evalSkills.js +258 -0
- package/dist/eval/evalSkills.js.map +1 -0
- package/dist/eval/semanticEval.js +148 -0
- package/dist/eval/semanticEval.js.map +1 -0
- package/dist/eval/semanticFixtures.js +227 -0
- package/dist/eval/semanticFixtures.js.map +1 -0
- package/dist/hooks/common.js +160 -0
- package/dist/hooks/common.js.map +1 -0
- package/dist/hooks/compaction.js +935 -0
- package/dist/hooks/compaction.js.map +1 -0
- package/dist/hooks/cursorPreToolUse.js +19 -0
- package/dist/hooks/cursorPreToolUse.js.map +1 -0
- package/dist/hooks/cursorSessionStart.js +71 -0
- package/dist/hooks/cursorSessionStart.js.map +1 -0
- package/dist/hooks/sessionStart.js +209 -0
- package/dist/hooks/sessionStart.js.map +1 -0
- package/dist/hooks/sessionStop.js +212 -0
- package/dist/hooks/sessionStop.js.map +1 -0
- package/dist/hooks/validateArtifact.js +933 -0
- package/dist/hooks/validateArtifact.js.map +1 -0
- package/dist/registries/artifactRegistry.js +206 -0
- package/dist/registries/artifactRegistry.js.map +1 -0
- package/dist/registries/capabilityContract.js +310 -0
- package/dist/registries/capabilityContract.js.map +1 -0
- package/dist/registries/packageRegistry.js +641 -0
- package/dist/registries/packageRegistry.js.map +1 -0
- package/dist/registries/runtimeAdapterRegistry.js +315 -0
- package/dist/registries/runtimeAdapterRegistry.js.map +1 -0
- package/dist/setup/codex.js +1056 -0
- package/dist/setup/codex.js.map +1 -0
- package/dist/setup/copilot.js +227 -0
- package/dist/setup/copilot.js.map +1 -0
- package/dist/setup/cursor.js +127 -0
- package/dist/setup/cursor.js.map +1 -0
- package/dist/setup/doctor.js +1276 -0
- package/dist/setup/doctor.js.map +1 -0
- package/dist/state/installRoot.js +279 -0
- package/dist/state/installRoot.js.map +1 -0
- package/dist/state/progressCommit.js +289 -0
- package/dist/state/progressCommit.js.map +1 -0
- package/dist/state/startupAnalysis.js +1953 -0
- package/dist/state/startupAnalysis.js.map +1 -0
- package/dist/upgrade/appModel.js +189 -0
- package/dist/upgrade/appModel.js.map +1 -0
- package/dist/upgrade/channels.js +208 -0
- package/dist/upgrade/channels.js.map +1 -0
- package/dist/upgrade/compatibility.js +201 -0
- package/dist/upgrade/compatibility.js.map +1 -0
- package/dist/upgrade/doctor.js +373 -0
- package/dist/upgrade/doctor.js.map +1 -0
- package/dist/upgrade/migrateArtifactsV2ToV3.js +332 -0
- package/dist/upgrade/migrateArtifactsV2ToV3.js.map +1 -0
- package/dist/upgrade/runtimeMigration.js +484 -0
- package/dist/upgrade/runtimeMigration.js.map +1 -0
- package/dist/upgrade/upgradeCommands.js +36 -0
- package/dist/upgrade/upgradeCommands.js.map +1 -0
- package/dist/upgrade/upgradeOrchestrator.js +299 -0
- package/dist/upgrade/upgradeOrchestrator.js.map +1 -0
- package/dist/upgrade/versionResolution.js +179 -0
- package/dist/upgrade/versionResolution.js.map +1 -0
- package/dist/validate/appHomeContract.js +150 -0
- package/dist/validate/appHomeContract.js.map +1 -0
- package/dist/validate/capability.js +412 -0
- package/dist/validate/capability.js.map +1 -0
- package/dist/validate/crossCapability.js +145 -0
- package/dist/validate/crossCapability.js.map +1 -0
- package/dist/validate/lifecycleAdapters.js +772 -0
- package/dist/validate/lifecycleAdapters.js.map +1 -0
- package/dist/validate/selfAudit.js +107 -0
- package/dist/validate/selfAudit.js.map +1 -0
- package/package.json +28 -8
- package/LICENSE +0 -201
- package/bin/agentera.mjs +0 -50
- package/lib/exec.mjs +0 -116
- package/lib/resolve.mjs +0 -129
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { loadYamlMapping, parseYaml } from "../core/yaml.js";
|
|
4
|
+
import { resolvePath } from "../core/paths.js";
|
|
5
|
+
import { resolveSourceRoot } from "../core/sourceRoot.js";
|
|
6
|
+
import { validateProgressCommits } from "../state/progressCommit.js";
|
|
7
|
+
import { DEFAULT_ARTIFACT_PATHS } from "./common.js";
|
|
8
|
+
import { COMPACTABLE_YAML_ARTIFACTS, compactFile, compactYamlFile } from "./compaction.js";
|
|
9
|
+
function schemasDirDefault() {
|
|
10
|
+
return path.join(resolveSourceRoot(), "skills", "agentera", "schemas", "artifacts");
|
|
11
|
+
}
|
|
12
|
+
const AGENT_YAML_RE = /\.agentera\/([a-z_]+)\.yaml$/;
|
|
13
|
+
const HUMAN_FACING = new Set(["TODO.md", "CHANGELOG.md", "DESIGN.md"]);
|
|
14
|
+
const HUMAN_FACING_SCHEMA_NAMES = {
|
|
15
|
+
"TODO.md": "todo",
|
|
16
|
+
"CHANGELOG.md": "changelog",
|
|
17
|
+
"DESIGN.md": "design",
|
|
18
|
+
};
|
|
19
|
+
const CANONICAL_SCHEMA_NAMES = {
|
|
20
|
+
"DECISIONS.md": "decisions",
|
|
21
|
+
"DOCS.md": "docs",
|
|
22
|
+
"EXPERIMENTS.md": "experiments",
|
|
23
|
+
"HEALTH.md": "health",
|
|
24
|
+
"PLAN.md": "plan",
|
|
25
|
+
"PROGRESS.md": "progress",
|
|
26
|
+
"VISION.md": "vision",
|
|
27
|
+
};
|
|
28
|
+
const ARTIFACT_BY_SCHEMA_NAME = Object.fromEntries(Object.entries(CANONICAL_SCHEMA_NAMES).map(([a, s]) => [s, a]));
|
|
29
|
+
const SKIP_META = new Set(["meta", "GROUP_PREFIXES", "BUDGET", "COMPACTION", "VALIDATION", "CONVENTION"]);
|
|
30
|
+
const LIST_INDICATORS = new Set(["number", "entry", "summary"]);
|
|
31
|
+
const SEQUENCE_KEYS_BY_ARTIFACT = {
|
|
32
|
+
decisions: { DECISION: "decisions", ARCHIVE: "archive" },
|
|
33
|
+
docs: { MAPPING: "mapping", INDEX: "index", AUDIT_LOG: "audit_log" },
|
|
34
|
+
experiments: { EXPERIMENT: "experiments", ARCHIVE: "archive" },
|
|
35
|
+
plan: { TASK: "tasks" },
|
|
36
|
+
progress: { CYCLE: "cycles", ARCHIVE: "archive" },
|
|
37
|
+
session: { BOOKMARK: "bookmarks" },
|
|
38
|
+
vision: { PERSONA: "personas", PRINCIPLE: "principles" },
|
|
39
|
+
};
|
|
40
|
+
const NESTED_SEQUENCE_KEYS = [[["DECISION", "ALTERNATIVE"], "alternatives"]];
|
|
41
|
+
const SEQUENCE_ORDER_BY_ARTIFACT = { "progress\0cycles": "descending" };
|
|
42
|
+
function isMapping(v) {
|
|
43
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
44
|
+
}
|
|
45
|
+
function pyTypeName(v) {
|
|
46
|
+
if (v === null || v === undefined)
|
|
47
|
+
return "NoneType";
|
|
48
|
+
if (typeof v === "boolean")
|
|
49
|
+
return "bool";
|
|
50
|
+
if (typeof v === "number")
|
|
51
|
+
return Number.isInteger(v) ? "int" : "float";
|
|
52
|
+
if (typeof v === "string")
|
|
53
|
+
return "str";
|
|
54
|
+
if (Array.isArray(v))
|
|
55
|
+
return "list";
|
|
56
|
+
if (typeof v === "object")
|
|
57
|
+
return "dict";
|
|
58
|
+
return typeof v;
|
|
59
|
+
}
|
|
60
|
+
function wordCount(text) {
|
|
61
|
+
return (text.match(/\S+/g) ?? []).length;
|
|
62
|
+
}
|
|
63
|
+
// ── Runtime event parsing ──────────────────────────────────────────
|
|
64
|
+
export class ArtifactWrite {
|
|
65
|
+
file_path;
|
|
66
|
+
content;
|
|
67
|
+
constructor(filePath, content = null) {
|
|
68
|
+
this.file_path = filePath;
|
|
69
|
+
this.content = content;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export class RuntimeEventParser {
|
|
73
|
+
parseClaude(data) {
|
|
74
|
+
const ti = data.tool_input;
|
|
75
|
+
if (!isMapping(ti))
|
|
76
|
+
return null;
|
|
77
|
+
const fp = ti.file_path;
|
|
78
|
+
if (fp)
|
|
79
|
+
return new ArtifactWrite(String(fp), ti.content ?? null);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
parseOpencode(data) {
|
|
83
|
+
const inp = data.input;
|
|
84
|
+
if (!isMapping(inp))
|
|
85
|
+
return null;
|
|
86
|
+
const fp = inp.path;
|
|
87
|
+
if (fp)
|
|
88
|
+
return new ArtifactWrite(String(fp), inp.content ?? null);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
parseCodex(data) {
|
|
92
|
+
const ti = data.tool_input;
|
|
93
|
+
if (!isMapping(ti))
|
|
94
|
+
return null;
|
|
95
|
+
const fp = ti.path;
|
|
96
|
+
const patchBody = ti.patch || ti.command || "";
|
|
97
|
+
if (fp)
|
|
98
|
+
return new ArtifactWrite(String(fp));
|
|
99
|
+
if (typeof patchBody === "string") {
|
|
100
|
+
const headers = [...patchBody.matchAll(/^\*\*\*\s+(?:Add File|Update File):\s+(.+?)\s*$/gm)];
|
|
101
|
+
if (headers.length > 0)
|
|
102
|
+
return new ArtifactWrite(headers[0][1]);
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
parseCopilot(data) {
|
|
107
|
+
const inp = data.input;
|
|
108
|
+
if (!isMapping(inp))
|
|
109
|
+
return null;
|
|
110
|
+
const fp = inp.filePath || inp.file_path;
|
|
111
|
+
if (fp)
|
|
112
|
+
return new ArtifactWrite(String(fp), inp.content ?? null);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
parse(data) {
|
|
116
|
+
const tn = data.tool_name ?? "";
|
|
117
|
+
if (tn === "apply_patch") {
|
|
118
|
+
const candidate = this.parseCodex(data);
|
|
119
|
+
if (candidate)
|
|
120
|
+
return candidate;
|
|
121
|
+
}
|
|
122
|
+
if (tn === "Edit" || tn === "Write" || (isMapping(data.tool_input) && "file_path" in data.tool_input)) {
|
|
123
|
+
const candidate = this.parseClaude(data);
|
|
124
|
+
if (candidate)
|
|
125
|
+
return candidate;
|
|
126
|
+
}
|
|
127
|
+
if (isMapping(data.input)) {
|
|
128
|
+
const inp = data.input;
|
|
129
|
+
if ("filePath" in inp || "file_path" in inp)
|
|
130
|
+
return this.parseCopilot(data);
|
|
131
|
+
if ("path" in inp)
|
|
132
|
+
return this.parseOpencode(data);
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ── Validation helpers ─────────────────────────────────────────────
|
|
138
|
+
function collectSingletonGroups(schema) {
|
|
139
|
+
const result = [];
|
|
140
|
+
for (const [gk, gv] of Object.entries(schema)) {
|
|
141
|
+
if (SKIP_META.has(gk) || !isMapping(gv))
|
|
142
|
+
continue;
|
|
143
|
+
let isListOrSub = false;
|
|
144
|
+
for (const e of Object.values(gv)) {
|
|
145
|
+
if (isMapping(e) && (LIST_INDICATORS.has(e.field) || e.parent)) {
|
|
146
|
+
isListOrSub = true;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (isListOrSub)
|
|
151
|
+
continue;
|
|
152
|
+
const fields = [];
|
|
153
|
+
for (const e of Object.values(gv)) {
|
|
154
|
+
if (isMapping(e) && e.required && "field" in e)
|
|
155
|
+
fields.push(e.field);
|
|
156
|
+
}
|
|
157
|
+
if (fields.length > 0)
|
|
158
|
+
result.push([gk, gk.toLowerCase(), fields]);
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
function isEmptyRequired(value) {
|
|
163
|
+
if (value === null || value === undefined)
|
|
164
|
+
return true;
|
|
165
|
+
if (typeof value === "string")
|
|
166
|
+
return !value.trim();
|
|
167
|
+
if (Array.isArray(value))
|
|
168
|
+
return value.length === 0;
|
|
169
|
+
if (typeof value === "object")
|
|
170
|
+
return Object.keys(value).length === 0;
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
function* iterGroupEntries(schema, group) {
|
|
174
|
+
const gv = schema[group];
|
|
175
|
+
if (!isMapping(gv))
|
|
176
|
+
return;
|
|
177
|
+
for (const entry of Object.values(gv)) {
|
|
178
|
+
if (isMapping(entry) && "field" in entry)
|
|
179
|
+
yield entry;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function validateField(violations, name, scope, field, p) {
|
|
183
|
+
const fullPath = p ? `${p}.${field}` : field;
|
|
184
|
+
if (!(field in scope)) {
|
|
185
|
+
violations.push(`${name}: missing required field '${fullPath}'`);
|
|
186
|
+
}
|
|
187
|
+
else if (isEmptyRequired(scope[field])) {
|
|
188
|
+
violations.push(`${name}: empty required field '${fullPath}'`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function allowedValues(entry) {
|
|
192
|
+
for (const rule of entry.validation ?? []) {
|
|
193
|
+
if (typeof rule === "string" && rule.startsWith("Must be one of: ")) {
|
|
194
|
+
return rule.slice("Must be one of: ".length).split(",").map((v) => v.trim());
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
function validateAllowedValue(violations, name, scope, entry, p) {
|
|
200
|
+
const field = entry.field;
|
|
201
|
+
const allowed = allowedValues(entry);
|
|
202
|
+
if (!field || allowed.length === 0 || !(field in scope) || isEmptyRequired(scope[field]))
|
|
203
|
+
return;
|
|
204
|
+
const value = scope[field];
|
|
205
|
+
if (typeof value === "string" && !allowed.includes(value)) {
|
|
206
|
+
violations.push(`${name}: invalid value '${value}' for '${p}.${field}' (expected one of: ${allowed.join(", ")})`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function validateFieldType(violations, name, scope, entry, p) {
|
|
210
|
+
const field = entry.field;
|
|
211
|
+
if (!field || !(field in scope))
|
|
212
|
+
return true;
|
|
213
|
+
const value = scope[field];
|
|
214
|
+
const expectedType = entry.type;
|
|
215
|
+
if (!expectedType || isEmptyRequired(value))
|
|
216
|
+
return true;
|
|
217
|
+
const fullPath = p ? `${p}.${field}` : field;
|
|
218
|
+
let isValid = true;
|
|
219
|
+
if (expectedType === "integer") {
|
|
220
|
+
if (typeof value === "boolean" || !(typeof value === "number" && Number.isInteger(value))) {
|
|
221
|
+
violations.push(`${name}: '${fullPath}' must be an integer, got ${pyTypeName(value)}`);
|
|
222
|
+
isValid = false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else if (expectedType === "string") {
|
|
226
|
+
if (typeof value !== "string") {
|
|
227
|
+
violations.push(`${name}: '${fullPath}' must be a string, got ${pyTypeName(value)}`);
|
|
228
|
+
isValid = false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else if (expectedType === "map") {
|
|
232
|
+
if (!isMapping(value)) {
|
|
233
|
+
violations.push(`${name}: '${fullPath}' must be a mapping, got ${pyTypeName(value)}`);
|
|
234
|
+
isValid = false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else if (expectedType === "list[string]") {
|
|
238
|
+
if (!Array.isArray(value) || !value.every((x) => typeof x === "string")) {
|
|
239
|
+
violations.push(`${name}: '${fullPath}' must be a list of strings`);
|
|
240
|
+
isValid = false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else if (expectedType === "list[map]") {
|
|
244
|
+
if (!Array.isArray(value) || !value.every((x) => isMapping(x))) {
|
|
245
|
+
violations.push(`${name}: '${fullPath}' must be a list of mappings`);
|
|
246
|
+
isValid = false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return isValid;
|
|
250
|
+
}
|
|
251
|
+
function validateFieldConstraints(violations, name, scope, entry, p) {
|
|
252
|
+
const field = entry.field;
|
|
253
|
+
if (!field || !(field in scope))
|
|
254
|
+
return;
|
|
255
|
+
const value = scope[field];
|
|
256
|
+
for (const rule of entry.validation ?? []) {
|
|
257
|
+
if (typeof rule !== "string")
|
|
258
|
+
continue;
|
|
259
|
+
if (rule === "Must be a positive integer") {
|
|
260
|
+
if (typeof value === "boolean" || !(typeof value === "number" && Number.isInteger(value)) || value <= 0) {
|
|
261
|
+
const fullPath = p ? `${p}.${field}` : field;
|
|
262
|
+
violations.push(`${name}: '${fullPath}' must be a positive integer`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function validateRequiredFields(violations, name, schema, group, scope, p) {
|
|
268
|
+
for (const entry of iterGroupEntries(schema, group)) {
|
|
269
|
+
const field = entry.field;
|
|
270
|
+
if (entry.parent || field === "entry")
|
|
271
|
+
continue;
|
|
272
|
+
if (entry.required)
|
|
273
|
+
validateField(violations, name, scope, field, p);
|
|
274
|
+
if (field in scope && !isEmptyRequired(scope[field])) {
|
|
275
|
+
if (validateFieldType(violations, name, scope, entry, p)) {
|
|
276
|
+
validateAllowedValue(violations, name, scope, entry, p);
|
|
277
|
+
validateFieldConstraints(violations, name, scope, entry, p);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const value = scope[field];
|
|
281
|
+
if (isMapping(value)) {
|
|
282
|
+
for (const child of entry.children ?? []) {
|
|
283
|
+
if (isMapping(child) && child.field) {
|
|
284
|
+
const childField = child.field;
|
|
285
|
+
const childPath = p ? `${p}.${field}` : field;
|
|
286
|
+
if (child.required)
|
|
287
|
+
validateField(violations, name, value, childField, childPath);
|
|
288
|
+
if (childField in value && !isEmptyRequired(value[childField])) {
|
|
289
|
+
if (validateFieldType(violations, name, value, child, childPath)) {
|
|
290
|
+
validateAllowedValue(violations, name, value, child, childPath);
|
|
291
|
+
validateFieldConstraints(violations, name, value, child, childPath);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function validateSingletonGroup(violations, name, schema, group, scope, p) {
|
|
300
|
+
for (const entry of iterGroupEntries(schema, group)) {
|
|
301
|
+
const field = entry.field;
|
|
302
|
+
if (entry.parent || field === "entry")
|
|
303
|
+
continue;
|
|
304
|
+
if (entry.required)
|
|
305
|
+
validateField(violations, name, scope, field, p);
|
|
306
|
+
if (field in scope && !isEmptyRequired(scope[field])) {
|
|
307
|
+
if (validateFieldType(violations, name, scope, entry, p)) {
|
|
308
|
+
validateAllowedValue(violations, name, scope, entry, p);
|
|
309
|
+
validateFieldConstraints(violations, name, scope, entry, p);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function schemaFieldNames(schema, group) {
|
|
315
|
+
const out = new Set();
|
|
316
|
+
for (const entry of iterGroupEntries(schema, group)) {
|
|
317
|
+
if (entry.field && !entry.parent && entry.field !== "entry")
|
|
318
|
+
out.add(entry.field);
|
|
319
|
+
}
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
function validateUnknownFields(violations, name, scope, allowed, p) {
|
|
323
|
+
for (const field of Object.keys(scope)) {
|
|
324
|
+
if (!allowed.has(field)) {
|
|
325
|
+
const fullPath = p ? `${p}.${field}` : field;
|
|
326
|
+
violations.push(`${name}: unsupported field '${fullPath}'`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function validatePlanKnownFields(data, schema, violations) {
|
|
331
|
+
const groupedScopes = { header: "HEADER", scope: "SCOPE" };
|
|
332
|
+
const sequenceKeys = new Set(Object.values(SEQUENCE_KEYS_BY_ARTIFACT.plan ?? {}));
|
|
333
|
+
const allowedTopLevel = new Set([
|
|
334
|
+
...schemaFieldNames(schema, "PLAN"),
|
|
335
|
+
...Object.keys(groupedScopes),
|
|
336
|
+
...sequenceKeys,
|
|
337
|
+
]);
|
|
338
|
+
validateUnknownFields(violations, "plan", data, allowedTopLevel, "");
|
|
339
|
+
for (const [key, group] of Object.entries(groupedScopes)) {
|
|
340
|
+
const scope = data[key];
|
|
341
|
+
if (isMapping(scope)) {
|
|
342
|
+
validateUnknownFields(violations, "plan", scope, schemaFieldNames(schema, group), key);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function validateFullPlanContract(data, violations) {
|
|
347
|
+
const header = isMapping(data.header) ? data.header : {};
|
|
348
|
+
if (String(header.level ?? "").toLowerCase() !== "full")
|
|
349
|
+
return;
|
|
350
|
+
for (const field of ["reviewed", "critic_issues"]) {
|
|
351
|
+
validateField(violations, "plan", header, field, "header");
|
|
352
|
+
}
|
|
353
|
+
validateField(violations, "plan", data, "design", "");
|
|
354
|
+
const criticIssues = header.critic_issues;
|
|
355
|
+
if (!isEmptyRequired(criticIssues)) {
|
|
356
|
+
const match = /^\s*(\d+)\s+found,\s*(\d+)\s+addressed,\s*(\d+)\s+dismissed\s*$/.exec(String(criticIssues));
|
|
357
|
+
if (!match) {
|
|
358
|
+
violations.push("plan: header.critic_issues must match 'N found, M addressed, K dismissed'");
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
const [found, addressed, dismissed] = [match[1], match[2], match[3]].map((v) => parseInt(v, 10));
|
|
362
|
+
if (found < 1)
|
|
363
|
+
violations.push("plan: header.critic_issues must record at least 1 found issue");
|
|
364
|
+
if (addressed + dismissed !== found) {
|
|
365
|
+
violations.push("plan: header.critic_issues counts must satisfy addressed + dismissed == found");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const tasks = data.tasks;
|
|
370
|
+
if (!Array.isArray(tasks))
|
|
371
|
+
return;
|
|
372
|
+
tasks.forEach((task, index) => {
|
|
373
|
+
if (isMapping(task))
|
|
374
|
+
validateField(violations, "plan", task, "acceptance", `tasks[${index}]`);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
function entryMinCount(schema, group) {
|
|
378
|
+
for (const entry of iterGroupEntries(schema, group)) {
|
|
379
|
+
if (entry.field === "entry" && entry.required)
|
|
380
|
+
return entry.min_count || 1;
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
function parentRequirements(schema, parentGroup) {
|
|
385
|
+
const requirements = {};
|
|
386
|
+
const prefix = `${parentGroup}.`;
|
|
387
|
+
for (const group of Object.keys(schema)) {
|
|
388
|
+
if (SKIP_META.has(group))
|
|
389
|
+
continue;
|
|
390
|
+
for (const entry of iterGroupEntries(schema, group)) {
|
|
391
|
+
const parent = entry.parent;
|
|
392
|
+
if (typeof parent === "string" &&
|
|
393
|
+
parent.startsWith(prefix) &&
|
|
394
|
+
parent !== `${group}.entry` &&
|
|
395
|
+
entry.required &&
|
|
396
|
+
entry.field) {
|
|
397
|
+
const parentField = parent.slice(prefix.length);
|
|
398
|
+
(requirements[parentField] ??= []).push(entry.field);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return requirements;
|
|
403
|
+
}
|
|
404
|
+
function entryRequirements(schema, group) {
|
|
405
|
+
const parent = `${group}.entry`;
|
|
406
|
+
const out = [];
|
|
407
|
+
for (const entry of iterGroupEntries(schema, group)) {
|
|
408
|
+
if (entry.parent === parent && entry.required && entry.field)
|
|
409
|
+
out.push(entry.field);
|
|
410
|
+
}
|
|
411
|
+
return out;
|
|
412
|
+
}
|
|
413
|
+
function validateSequences(data, schema, name, violations) {
|
|
414
|
+
for (const [group, key] of Object.entries(SEQUENCE_KEYS_BY_ARTIFACT[name] ?? {})) {
|
|
415
|
+
const seq = data[key];
|
|
416
|
+
if (seq === null || seq === undefined)
|
|
417
|
+
continue;
|
|
418
|
+
if (!Array.isArray(seq)) {
|
|
419
|
+
violations.push(`${name}: '${key}' must be a list`);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (seq.length === 0) {
|
|
423
|
+
violations.push(`${name}: '${key}' requires at least 1 entry`);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const childRequirements = parentRequirements(schema, group);
|
|
427
|
+
seq.forEach((item, index) => {
|
|
428
|
+
const p = `${key}[${index}]`;
|
|
429
|
+
if (!isMapping(item)) {
|
|
430
|
+
violations.push(`${name}: '${p}' must be a mapping`);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
validateRequiredFields(violations, name, schema, group, item, p);
|
|
434
|
+
for (const [parentField, childFields] of Object.entries(childRequirements)) {
|
|
435
|
+
const childScope = item[parentField];
|
|
436
|
+
if (!isMapping(childScope))
|
|
437
|
+
continue;
|
|
438
|
+
for (const childField of childFields) {
|
|
439
|
+
validateField(violations, name, childScope, childField, `${p}.${parentField}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
for (const [[parentGroup, childGroup], childKey] of NESTED_SEQUENCE_KEYS) {
|
|
443
|
+
if (parentGroup !== group)
|
|
444
|
+
continue;
|
|
445
|
+
const childSeq = item[childKey];
|
|
446
|
+
const minCount = entryMinCount(schema, childGroup);
|
|
447
|
+
if (minCount && (!Array.isArray(childSeq) || childSeq.length < minCount)) {
|
|
448
|
+
violations.push(`${name}: '${p}.${childKey}' requires at least ${minCount} entry`);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (!Array.isArray(childSeq))
|
|
452
|
+
continue;
|
|
453
|
+
const required = entryRequirements(schema, childGroup);
|
|
454
|
+
childSeq.forEach((child, childIndex) => {
|
|
455
|
+
const childPath = `${p}.${childKey}[${childIndex}]`;
|
|
456
|
+
if (!isMapping(child)) {
|
|
457
|
+
violations.push(`${name}: '${childPath}' must be a mapping`);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
for (const field of required)
|
|
461
|
+
validateField(violations, name, child, field, childPath);
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function validateDecisionAlternatives(data, name) {
|
|
468
|
+
const violations = [];
|
|
469
|
+
(data.decisions ?? []).forEach((decision, index) => {
|
|
470
|
+
if (!isMapping(decision))
|
|
471
|
+
return;
|
|
472
|
+
const alternatives = decision.alternatives ?? [];
|
|
473
|
+
if (!Array.isArray(alternatives))
|
|
474
|
+
return;
|
|
475
|
+
const chosen = alternatives.filter((alt) => isMapping(alt) && alt.status === "chosen");
|
|
476
|
+
if (chosen.length !== 1) {
|
|
477
|
+
violations.push(`${name}: 'decisions[${index}].alternatives' must have exactly one chosen entry`);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
return violations;
|
|
481
|
+
}
|
|
482
|
+
function validateDecisionSatisfaction(data, name) {
|
|
483
|
+
const violations = [];
|
|
484
|
+
const allowedStates = new Set(["open", "provisionally_satisfied", "user_confirmed_satisfied"]);
|
|
485
|
+
const entries = [
|
|
486
|
+
...(data.decisions ?? []).map((d, i) => [`decisions[${i}]`, d]),
|
|
487
|
+
...(data.archive ?? []).map((d, i) => [`archive[${i}]`, d]),
|
|
488
|
+
];
|
|
489
|
+
for (const [entryPath, decision] of entries) {
|
|
490
|
+
if (!isMapping(decision) || !("satisfaction" in decision))
|
|
491
|
+
continue;
|
|
492
|
+
const p = `${entryPath}.satisfaction`;
|
|
493
|
+
const satisfaction = decision.satisfaction;
|
|
494
|
+
if (!isMapping(satisfaction)) {
|
|
495
|
+
violations.push(`${name}: '${p}' must be a mapping`);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
validateUnknownFields(violations, name, satisfaction, new Set(["state", "evidence", "user_confirmation"]), p);
|
|
499
|
+
const state = satisfaction.state;
|
|
500
|
+
if (typeof state !== "string" || !state.trim()) {
|
|
501
|
+
violations.push(`${name}: missing required field '${p}.state'`);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (!allowedStates.has(state)) {
|
|
505
|
+
violations.push(`${name}: invalid value '${state}' for '${p}.state' (expected one of: open, provisionally_satisfied, user_confirmed_satisfied)`);
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (state === "provisionally_satisfied" && isEmptyRequired(satisfaction.evidence)) {
|
|
509
|
+
violations.push(`${name}: '${p}.evidence' is required for provisionally_satisfied`);
|
|
510
|
+
}
|
|
511
|
+
if (state === "user_confirmed_satisfied") {
|
|
512
|
+
const confirmation = satisfaction.user_confirmation;
|
|
513
|
+
if (confirmation === null || confirmation === undefined) {
|
|
514
|
+
violations.push(`${name}: '${p}.user_confirmation' is required for user_confirmed_satisfied`);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (!isMapping(confirmation)) {
|
|
518
|
+
violations.push(`${name}: '${p}.user_confirmation' must be a mapping with confirmed_by and confirmed_at, got ${pyTypeName(confirmation)}`);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
for (const field of ["confirmed_by", "confirmed_at"]) {
|
|
522
|
+
if (isEmptyRequired(confirmation[field])) {
|
|
523
|
+
violations.push(`${name}: missing required field '${p}.user_confirmation.${field}'`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return violations;
|
|
529
|
+
}
|
|
530
|
+
function validationRuleSeverity(schema, rule) {
|
|
531
|
+
for (const groupKey of ["VALIDATION", "VALIDATION_RULES"]) {
|
|
532
|
+
for (const entry of Object.values(schema[groupKey] ?? {})) {
|
|
533
|
+
if (isMapping(entry) && entry.rule === rule) {
|
|
534
|
+
const severity = entry.severity;
|
|
535
|
+
return severity ? String(severity) : null;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
function expectedSequenceOrder(name, key) {
|
|
542
|
+
return SEQUENCE_ORDER_BY_ARTIFACT[`${name}\0${key}`] ?? "ascending";
|
|
543
|
+
}
|
|
544
|
+
function sequenceInOrder(nums, direction) {
|
|
545
|
+
const reverse = direction === "descending";
|
|
546
|
+
const sorted = [...nums].sort((a, b) => (reverse ? b - a : a - b));
|
|
547
|
+
return nums.length === sorted.length && nums.every((n, i) => n === sorted[i]);
|
|
548
|
+
}
|
|
549
|
+
function validateYamlContent(content, schema, name) {
|
|
550
|
+
const violations = [];
|
|
551
|
+
let data;
|
|
552
|
+
try {
|
|
553
|
+
data = parseYaml(content);
|
|
554
|
+
}
|
|
555
|
+
catch (exc) {
|
|
556
|
+
return [`${name}: invalid YAML: ${exc.message}`];
|
|
557
|
+
}
|
|
558
|
+
if (!isMapping(data)) {
|
|
559
|
+
return [`${name}: root must be a mapping`];
|
|
560
|
+
}
|
|
561
|
+
for (const [group, groupLower, fields] of collectSingletonGroups(schema)) {
|
|
562
|
+
let scope;
|
|
563
|
+
if (groupLower in data && isMapping(data[groupLower])) {
|
|
564
|
+
scope = data[groupLower];
|
|
565
|
+
}
|
|
566
|
+
else if (fields.some((f) => f in data)) {
|
|
567
|
+
scope = data;
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
validateSingletonGroup(violations, name, schema, group, scope, groupLower);
|
|
573
|
+
}
|
|
574
|
+
if (name === "plan") {
|
|
575
|
+
validatePlanKnownFields(data, schema, violations);
|
|
576
|
+
validateFullPlanContract(data, violations);
|
|
577
|
+
}
|
|
578
|
+
validateSequences(data, schema, name, violations);
|
|
579
|
+
if (name === "decisions") {
|
|
580
|
+
violations.push(...validateDecisionAlternatives(data, name));
|
|
581
|
+
violations.push(...validateDecisionSatisfaction(data, name));
|
|
582
|
+
}
|
|
583
|
+
const wordBudgetSeverity = validationRuleSeverity(schema, "word_budget");
|
|
584
|
+
for (const be of Object.values(schema.BUDGET ?? {})) {
|
|
585
|
+
if (!isMapping(be))
|
|
586
|
+
continue;
|
|
587
|
+
const mw = be.max_words;
|
|
588
|
+
const scope = be.scope ?? "";
|
|
589
|
+
if (mw && String(scope).includes("full_file") && wordBudgetSeverity === "error") {
|
|
590
|
+
const wc = wordCount(content);
|
|
591
|
+
if (wc > mw)
|
|
592
|
+
violations.push(`${name}: word count (${wc}) exceeds budget (${mw})`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
for (const groupKey of ["VALIDATION", "VALIDATION_RULES"]) {
|
|
596
|
+
for (const ve of Object.values(schema[groupKey] ?? {})) {
|
|
597
|
+
if (!isMapping(ve))
|
|
598
|
+
continue;
|
|
599
|
+
const rule = ve.rule ?? "";
|
|
600
|
+
if (ve.severity !== "error")
|
|
601
|
+
continue;
|
|
602
|
+
if (rule.includes("unique") && rule.includes("number")) {
|
|
603
|
+
for (const [key, val] of Object.entries(data)) {
|
|
604
|
+
if (Array.isArray(val)) {
|
|
605
|
+
const nums = val.filter((e) => isMapping(e) && "number" in e).map((e) => e.number);
|
|
606
|
+
if (nums.length > 0) {
|
|
607
|
+
if (nums.length !== new Set(nums).size) {
|
|
608
|
+
violations.push(`${name}: duplicate numbers in '${key}'`);
|
|
609
|
+
}
|
|
610
|
+
const direction = expectedSequenceOrder(name, key);
|
|
611
|
+
if (!sequenceInOrder(nums, direction)) {
|
|
612
|
+
violations.push(`${name}: '${key}' not in ${direction} order`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
else if (rule === "closure_consistency") {
|
|
619
|
+
const header = isMapping(data.header) ? data.header : {};
|
|
620
|
+
const status = data.status ?? header.status;
|
|
621
|
+
if (typeof status === "string" && status === "closed") {
|
|
622
|
+
for (const field of ["closed_at", "final_value", "target_ref", "reason"]) {
|
|
623
|
+
let val = data[field];
|
|
624
|
+
if (val === null || val === undefined)
|
|
625
|
+
val = header[field];
|
|
626
|
+
if (val === null || val === undefined || (typeof val === "string" && !val.trim())) {
|
|
627
|
+
violations.push(`${name}: closure field '${field}' is required when status is 'closed'`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return violations;
|
|
635
|
+
}
|
|
636
|
+
function validateMd(content, name, schema = null) {
|
|
637
|
+
const violations = [];
|
|
638
|
+
if (!content.trim())
|
|
639
|
+
violations.push(`${name}: empty content`);
|
|
640
|
+
const fences = (content.match(/^```/gm) ?? []).length;
|
|
641
|
+
if (fences % 2)
|
|
642
|
+
violations.push(`${name}: unclosed code fence`);
|
|
643
|
+
if (schema)
|
|
644
|
+
violations.push(...validateMdSchema(content, name, schema));
|
|
645
|
+
return violations;
|
|
646
|
+
}
|
|
647
|
+
function validateMdSchema(content, name, schema) {
|
|
648
|
+
const violations = [];
|
|
649
|
+
if (!content.trim())
|
|
650
|
+
return violations;
|
|
651
|
+
for (const [groupKey, groupValue] of Object.entries(schema)) {
|
|
652
|
+
if (SKIP_META.has(groupKey) || !isMapping(groupValue))
|
|
653
|
+
continue;
|
|
654
|
+
const hasRequired = Object.values(groupValue).some((e) => isMapping(e) && e.required && e.field);
|
|
655
|
+
if (!hasRequired)
|
|
656
|
+
continue;
|
|
657
|
+
if (groupKey === "ITEM")
|
|
658
|
+
validateMdItems(content, name, violations);
|
|
659
|
+
else if (groupKey === "RELEASE")
|
|
660
|
+
validateMdReleases(content, name, violations);
|
|
661
|
+
else if (groupKey === "TOKEN")
|
|
662
|
+
validateMdTokens(content, name, violations);
|
|
663
|
+
}
|
|
664
|
+
return violations;
|
|
665
|
+
}
|
|
666
|
+
function validateMdItems(content, name, violations) {
|
|
667
|
+
const versionHeading = /^##\s+/m.exec(content);
|
|
668
|
+
if (!versionHeading) {
|
|
669
|
+
violations.push(`${name}: missing severity sections (expected '## <glyph> <name>' headings)`);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const severityGlyphs = ["⇶", "⇉", "→", "⇢"];
|
|
673
|
+
let found = false;
|
|
674
|
+
for (const glyph of severityGlyphs) {
|
|
675
|
+
const g = glyph.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
676
|
+
if (new RegExp(`^##\\s*${g}`, "m").test(content)) {
|
|
677
|
+
found = true;
|
|
678
|
+
const sectionStart = new RegExp(`^##\\s*${g}.+$`, "m").exec(content);
|
|
679
|
+
if (sectionStart) {
|
|
680
|
+
const idx = sectionStart.index + sectionStart[0].length;
|
|
681
|
+
const nextSection = content.indexOf("\n##", idx);
|
|
682
|
+
const sectionBody = nextSection >= 0 ? content.slice(idx, nextSection) : content.slice(idx);
|
|
683
|
+
if (!/^\s*-/m.test(sectionBody)) {
|
|
684
|
+
const headingSlice = content.slice(sectionStart.index, sectionStart.index + sectionStart[0].length);
|
|
685
|
+
const glyphNameMatch = new RegExp(`^##\\s*(${g}.+)$`, "m").exec(headingSlice);
|
|
686
|
+
const headingText = glyphNameMatch ? glyphNameMatch[1] : glyph;
|
|
687
|
+
violations.push(`${name}: severity section '${headingText}' has no list entries (expected '- [type]' items)`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (!found) {
|
|
693
|
+
violations.push(`${name}: missing severity glyph in section headings (expected '## ⇶ Critical', '## ⇉ Degraded', '## → Normal', '## ⇢ Annoying')`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function validateMdReleases(content, name, violations) {
|
|
697
|
+
if (!/^##\s*\[/m.test(content)) {
|
|
698
|
+
violations.push(`${name}: missing version header (expected '## [X.Y.Z]')`);
|
|
699
|
+
}
|
|
700
|
+
const changeSections = new Set(["### Added", "### Changed", "### Fixed", "### Removed"]);
|
|
701
|
+
const lines = new Set(content.split("\n"));
|
|
702
|
+
if (![...changeSections].some((s) => lines.has(s))) {
|
|
703
|
+
violations.push(`${name}: missing change sections (expected '### Added', '### Changed', '### Fixed', or '### Removed')`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function validateMdTokens(content, name, violations) {
|
|
707
|
+
if (!/^##\s/m.test(content)) {
|
|
708
|
+
violations.push(`${name}: missing section heading (expected '## SectionName')`);
|
|
709
|
+
}
|
|
710
|
+
const yamlBlocks = (content.match(/^```yaml\s*$/gm) ?? []).length;
|
|
711
|
+
if (!yamlBlocks) {
|
|
712
|
+
violations.push(`${name}: missing YAML code block with token definitions (expected '\`\`\`yaml')`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// ── Path resolution ────────────────────────────────────────────────
|
|
716
|
+
function resolvePathRel(fp, cwd) {
|
|
717
|
+
return path.isAbsolute(fp) ? fp : path.join(cwd, fp);
|
|
718
|
+
}
|
|
719
|
+
function docsPathOverrides(cwd) {
|
|
720
|
+
const docsPath = path.join(cwd, ".agentera", "docs.yaml");
|
|
721
|
+
if (!fs.existsSync(docsPath) || !fs.statSync(docsPath).isFile())
|
|
722
|
+
return {};
|
|
723
|
+
let data;
|
|
724
|
+
try {
|
|
725
|
+
data = loadYamlMapping(fs.readFileSync(docsPath, "utf8"));
|
|
726
|
+
}
|
|
727
|
+
catch (exc) {
|
|
728
|
+
process.stderr.write(`warning: failed to load docs path overrides: ${exc.message}\n`);
|
|
729
|
+
return {};
|
|
730
|
+
}
|
|
731
|
+
const mapping = data.mapping;
|
|
732
|
+
if (!Array.isArray(mapping))
|
|
733
|
+
return {};
|
|
734
|
+
const overrides = {};
|
|
735
|
+
for (const entry of mapping) {
|
|
736
|
+
if (!isMapping(entry))
|
|
737
|
+
continue;
|
|
738
|
+
const artifact = entry.artifact;
|
|
739
|
+
const p = entry.path;
|
|
740
|
+
if (typeof artifact === "string" && typeof p === "string")
|
|
741
|
+
overrides[artifact] = p;
|
|
742
|
+
}
|
|
743
|
+
return overrides;
|
|
744
|
+
}
|
|
745
|
+
function defaultArtifactPath(artifact, cwd) {
|
|
746
|
+
const rel = docsPathOverrides(cwd)[artifact] ?? DEFAULT_ARTIFACT_PATHS[artifact] ?? "";
|
|
747
|
+
return rel ? resolvePathRel(rel, cwd) : "";
|
|
748
|
+
}
|
|
749
|
+
function artifactPaths(cwd) {
|
|
750
|
+
const paths = { ...DEFAULT_ARTIFACT_PATHS, ...docsPathOverrides(cwd) };
|
|
751
|
+
const resolved = {};
|
|
752
|
+
for (const [artifact, p] of Object.entries(paths))
|
|
753
|
+
resolved[artifact] = resolvePathRel(p, cwd);
|
|
754
|
+
return resolved;
|
|
755
|
+
}
|
|
756
|
+
function samePath(left, right) {
|
|
757
|
+
return resolvePath(left) === resolvePath(right);
|
|
758
|
+
}
|
|
759
|
+
function artifactForWrite(absPath, relPath, basename, cwd) {
|
|
760
|
+
for (const [artifact, mappedPath] of Object.entries(artifactPaths(cwd))) {
|
|
761
|
+
if (samePath(absPath, mappedPath))
|
|
762
|
+
return artifact;
|
|
763
|
+
}
|
|
764
|
+
const match = AGENT_YAML_RE.exec(relPath);
|
|
765
|
+
if (match)
|
|
766
|
+
return ARTIFACT_BY_SCHEMA_NAME[match[1]] ?? null;
|
|
767
|
+
if (HUMAN_FACING.has(basename))
|
|
768
|
+
return basename;
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
function readIfNeeded(content, absPath) {
|
|
772
|
+
if (content !== null && content !== undefined)
|
|
773
|
+
return content;
|
|
774
|
+
try {
|
|
775
|
+
return fs.readFileSync(absPath, "utf8");
|
|
776
|
+
}
|
|
777
|
+
catch {
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
function compactAfterValidWrite(artifact, absPath) {
|
|
782
|
+
if (!fs.existsSync(absPath))
|
|
783
|
+
return [];
|
|
784
|
+
try {
|
|
785
|
+
if (artifact === "TODO.md") {
|
|
786
|
+
compactFile(absPath, "todo-resolved");
|
|
787
|
+
}
|
|
788
|
+
else if (artifact in COMPACTABLE_YAML_ARTIFACTS) {
|
|
789
|
+
compactYamlFile(absPath, artifact);
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
return [];
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
catch (exc) {
|
|
796
|
+
return [`${artifact}: compaction failed: ${exc.message}`];
|
|
797
|
+
}
|
|
798
|
+
return [];
|
|
799
|
+
}
|
|
800
|
+
// ── Validator + adapter ────────────────────────────────────────────
|
|
801
|
+
export class ArtifactSchemaValidator {
|
|
802
|
+
schemasDir;
|
|
803
|
+
schemaCache;
|
|
804
|
+
constructor(schemasDir = schemasDirDefault()) {
|
|
805
|
+
this.schemasDir = schemasDir;
|
|
806
|
+
this.schemaCache = new Map();
|
|
807
|
+
}
|
|
808
|
+
loadSchema(name) {
|
|
809
|
+
if (!this.schemaCache.has(name)) {
|
|
810
|
+
const p = path.join(this.schemasDir, `${name}.yaml`);
|
|
811
|
+
if (fs.existsSync(p) && fs.statSync(p).isFile()) {
|
|
812
|
+
this.schemaCache.set(name, loadYamlMapping(fs.readFileSync(p, "utf8")));
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
this.schemaCache.set(name, null);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return this.schemaCache.get(name) ?? null;
|
|
819
|
+
}
|
|
820
|
+
validateYaml(content, schema, name) {
|
|
821
|
+
return validateYamlContent(content, schema, name);
|
|
822
|
+
}
|
|
823
|
+
validateMarkdown(content, name, schema = null) {
|
|
824
|
+
return validateMd(content, name, schema);
|
|
825
|
+
}
|
|
826
|
+
validateWrite(write, cwd) {
|
|
827
|
+
const absPath = resolvePathRel(write.file_path, cwd);
|
|
828
|
+
const rel = path.relative(cwd, absPath).replace(/\\/g, "/");
|
|
829
|
+
const basename = path.basename(absPath);
|
|
830
|
+
const artifact = artifactForWrite(absPath, rel, basename, cwd);
|
|
831
|
+
if (artifact && artifact in CANONICAL_SCHEMA_NAMES) {
|
|
832
|
+
const name = CANONICAL_SCHEMA_NAMES[artifact];
|
|
833
|
+
const schema = this.loadSchema(name);
|
|
834
|
+
if (schema === null)
|
|
835
|
+
return [];
|
|
836
|
+
if (Object.keys(schema).length === 0)
|
|
837
|
+
return [`${name}: schema file is empty or contains no valid definitions`];
|
|
838
|
+
const content = readIfNeeded(write.content, absPath);
|
|
839
|
+
if (content === null)
|
|
840
|
+
return [];
|
|
841
|
+
let violations = this.validateYaml(content, schema, name);
|
|
842
|
+
if (name === "progress")
|
|
843
|
+
violations = violations.concat(validateProgressCommits(content, cwd));
|
|
844
|
+
if (violations.length > 0)
|
|
845
|
+
return violations;
|
|
846
|
+
return compactAfterValidWrite(artifact, absPath);
|
|
847
|
+
}
|
|
848
|
+
if (artifact && HUMAN_FACING.has(artifact)) {
|
|
849
|
+
const content = readIfNeeded(write.content, absPath);
|
|
850
|
+
if (content === null)
|
|
851
|
+
return [];
|
|
852
|
+
const schemaName = HUMAN_FACING_SCHEMA_NAMES[artifact];
|
|
853
|
+
const schema = schemaName ? this.loadSchema(schemaName) : null;
|
|
854
|
+
const violations = this.validateMarkdown(content, artifact, schema);
|
|
855
|
+
if (violations.length > 0)
|
|
856
|
+
return violations;
|
|
857
|
+
return compactAfterValidWrite(artifact, absPath);
|
|
858
|
+
}
|
|
859
|
+
return [];
|
|
860
|
+
}
|
|
861
|
+
validateExplicit(artifact, filePath, cwd) {
|
|
862
|
+
const content = readIfNeeded(null, filePath);
|
|
863
|
+
if (content === null)
|
|
864
|
+
return [`${artifact}: cannot read artifact file '${filePath}'`];
|
|
865
|
+
if (artifact in CANONICAL_SCHEMA_NAMES) {
|
|
866
|
+
const name = CANONICAL_SCHEMA_NAMES[artifact];
|
|
867
|
+
const schema = this.loadSchema(name);
|
|
868
|
+
if (schema === null)
|
|
869
|
+
return [`${artifact}: schema '${name}' is not available`];
|
|
870
|
+
if (Object.keys(schema).length === 0)
|
|
871
|
+
return [`${artifact}: schema '${name}' file is empty or contains no valid definitions`];
|
|
872
|
+
let violations = this.validateYaml(content, schema, name);
|
|
873
|
+
if (name === "progress")
|
|
874
|
+
violations = violations.concat(validateProgressCommits(content, cwd));
|
|
875
|
+
return violations;
|
|
876
|
+
}
|
|
877
|
+
if (HUMAN_FACING.has(artifact)) {
|
|
878
|
+
const schemaName = HUMAN_FACING_SCHEMA_NAMES[artifact];
|
|
879
|
+
const schema = schemaName ? this.loadSchema(schemaName) : null;
|
|
880
|
+
return this.validateMarkdown(content, artifact, schema);
|
|
881
|
+
}
|
|
882
|
+
return [
|
|
883
|
+
`${artifact}: unsupported artifact; expected one of: ${Object.keys(DEFAULT_ARTIFACT_PATHS).sort().join(", ")}`,
|
|
884
|
+
];
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
export function loadSchema(name) {
|
|
888
|
+
return new ArtifactSchemaValidator().loadSchema(name);
|
|
889
|
+
}
|
|
890
|
+
export class HookCliAdapter {
|
|
891
|
+
parser;
|
|
892
|
+
validator;
|
|
893
|
+
constructor(parser, validator) {
|
|
894
|
+
this.parser = parser ?? new RuntimeEventParser();
|
|
895
|
+
this.validator = validator ?? new ArtifactSchemaValidator();
|
|
896
|
+
}
|
|
897
|
+
run(raw, defaultCwd = null) {
|
|
898
|
+
let data;
|
|
899
|
+
try {
|
|
900
|
+
if (!raw.trim())
|
|
901
|
+
return [0, []];
|
|
902
|
+
data = JSON.parse(raw);
|
|
903
|
+
}
|
|
904
|
+
catch {
|
|
905
|
+
return [0, []];
|
|
906
|
+
}
|
|
907
|
+
if (!isMapping(data))
|
|
908
|
+
return [0, []];
|
|
909
|
+
const write = this.parser.parse(data);
|
|
910
|
+
if (write === null)
|
|
911
|
+
return [0, []];
|
|
912
|
+
const cwd = data.cwd ?? defaultCwd ?? process.cwd();
|
|
913
|
+
const violations = this.validator.validateWrite(write, cwd);
|
|
914
|
+
return violations.length > 0 ? [2, violations] : [0, []];
|
|
915
|
+
}
|
|
916
|
+
runExplicit(artifact, filePath, cwd) {
|
|
917
|
+
artifact = artifact.trim();
|
|
918
|
+
const defaultPath = defaultArtifactPath(artifact, cwd);
|
|
919
|
+
const resolvedFile = filePath ? resolvePathRel(filePath, cwd) : defaultPath;
|
|
920
|
+
const violations = this.validator.validateExplicit(artifact, resolvedFile, cwd);
|
|
921
|
+
const payload = {
|
|
922
|
+
command: "validate-artifact",
|
|
923
|
+
status: violations.length > 0 ? "fail" : "pass",
|
|
924
|
+
artifact,
|
|
925
|
+
file: resolvedFile,
|
|
926
|
+
docs_mapped_default: defaultPath || null,
|
|
927
|
+
path_source: filePath ? "provided" : "docs_mapped_default",
|
|
928
|
+
violations,
|
|
929
|
+
};
|
|
930
|
+
return violations.length > 0 ? [2, payload] : [0, payload];
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
//# sourceMappingURL=validateArtifact.js.map
|