@agilno-tech/rivet 0.1.0-alpha.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/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +59 -0
- package/bin/cli.js +42 -0
- package/dist/agents/backend-django-agent/SKILL.md +35 -0
- package/dist/agents/backend-nestjs-agent/SKILL.md +36 -0
- package/dist/agents/boss-agent/SKILL.md +28 -0
- package/dist/agents/bounded-worker/SKILL.md +28 -0
- package/dist/agents/data-performance-agent/SKILL.md +32 -0
- package/dist/agents/delivery-ticketing-agent/SKILL.md +36 -0
- package/dist/agents/devops-agent/SKILL.md +34 -0
- package/dist/agents/engineering-manager/SKILL.md +28 -0
- package/dist/agents/frontend-nextjs-agent/SKILL.md +34 -0
- package/dist/agents/frontend-web-agent/SKILL.md +31 -0
- package/dist/agents/incident-response-agent/SKILL.md +42 -0
- package/dist/agents/jest-agent/SKILL.md +77 -0
- package/dist/agents/mobile-agent/SKILL.md +32 -0
- package/dist/agents/playwright-agent/SKILL.md +54 -0
- package/dist/agents/product-design-manager/SKILL.md +27 -0
- package/dist/agents/qa-agent/SKILL.md +29 -0
- package/dist/agents/quality-manager/SKILL.md +28 -0
- package/dist/agents/vitest-agent/SKILL.md +77 -0
- package/dist/governance/pre-push-rules.md +75 -0
- package/dist/governance/prompt-hygiene.md +22 -0
- package/dist/governance/review-checklist.md +24 -0
- package/dist/governance/safety-and-data.md +27 -0
- package/dist/governance/usage-rules.md +21 -0
- package/dist/mandatory/address-pr-feedback/SKILL.md +196 -0
- package/dist/mandatory/agentic-goal/SKILL.md +42 -0
- package/dist/mandatory/agentic-status/SKILL.md +74 -0
- package/dist/mandatory/apply-design/SKILL.md +228 -0
- package/dist/mandatory/check-ac/SKILL.md +124 -0
- package/dist/mandatory/create-pr-and-commit/SKILL.md +304 -0
- package/dist/mandatory/design/SKILL.md +312 -0
- package/dist/mandatory/feature-workflow/SKILL.md +87 -0
- package/dist/mandatory/hotfix/SKILL.md +195 -0
- package/dist/mandatory/pre-push/SKILL.md +272 -0
- package/dist/mandatory/project-context/SKILL.md +180 -0
- package/dist/mandatory/release-docs/SKILL.md +191 -0
- package/dist/mandatory/review-pr/SKILL.md +323 -0
- package/dist/mandatory/security-review/SKILL.md +157 -0
- package/dist/skills/api-contract/SKILL.md +77 -0
- package/dist/skills/backend-django/SKILL.md +27 -0
- package/dist/skills/backend-nestjs/SKILL.md +65 -0
- package/dist/skills/bug-ticket-creation/SKILL.md +43 -0
- package/dist/skills/cloudwatch-troubleshooting/SKILL.md +73 -0
- package/dist/skills/database-migration/SKILL.md +97 -0
- package/dist/skills/debugging/SKILL.md +32 -0
- package/dist/skills/devops-infra/SKILL.md +26 -0
- package/dist/skills/documentation/SKILL.md +24 -0
- package/dist/skills/frontend-nextjs/SKILL.md +27 -0
- package/dist/skills/incident-postmortem/SKILL.md +38 -0
- package/dist/skills/kubernetes-troubleshooting/SKILL.md +71 -0
- package/dist/skills/mobile-react-native/SKILL.md +37 -0
- package/dist/skills/postgres-analytics/SKILL.md +29 -0
- package/dist/skills/product-jira-ticketing/SKILL.md +28 -0
- package/dist/skills/qa-bug-analysis/SKILL.md +27 -0
- package/dist/skills/refactoring/SKILL.md +32 -0
- package/dist/skills/sprint-planning/SKILL.md +32 -0
- package/dist/skills/testing-quality/SKILL.md +62 -0
- package/dist/v2/protocols/agent-orchestration.md +49 -0
- package/dist/v2/protocols/delivery.md +45 -0
- package/dist/v2/protocols/design-authority.md +29 -0
- package/dist/v2/protocols/goal-graph.md +44 -0
- package/dist/v2/protocols/pattern-first-development.md +29 -0
- package/dist/v2/protocols/qa-evidence.md +29 -0
- package/dist/v2/protocols/security.md +29 -0
- package/dist/v2/schemas/event.schema.json +86 -0
- package/dist/v2/schemas/evidence.schema.json +94 -0
- package/dist/v2/schemas/feature-decomposition.schema.json +48 -0
- package/dist/v2/schemas/feature-plan.schema.json +95 -0
- package/dist/v2/schemas/goal-graph.schema.json +61 -0
- package/dist/v2/schemas/integration.schema.json +119 -0
- package/dist/v2/schemas/orchestration.schema.json +69 -0
- package/dist/v2/schemas/project.schema.json +209 -0
- package/dist/v2/schemas/providers.schema.json +142 -0
- package/dist/v2/schemas/quality.schema.json +50 -0
- package/dist/v2/schemas/work-action.schema.json +44 -0
- package/dist/v2/schemas/work-request.schema.json +253 -0
- package/dist/v2/templates/evidence/qa-bundle.json +194 -0
- package/dist/v2/templates/github-actions/rivet-deploy.yml +98 -0
- package/dist/v2/templates/harness/SKILL.md +90 -0
- package/dist/v2/templates/project/.rivet/orchestration.yaml +42 -0
- package/dist/v2/templates/project/.rivet/project.yaml +14 -0
- package/dist/v2/templates/project/.rivet/providers.yaml +8 -0
- package/dist/v2/templates/project/.rivet/quality.yaml +16 -0
- package/package.json +58 -0
- package/protocols/agent-orchestration.md +49 -0
- package/protocols/delivery.md +45 -0
- package/protocols/design-authority.md +29 -0
- package/protocols/goal-graph.md +44 -0
- package/protocols/pattern-first-development.md +29 -0
- package/protocols/qa-evidence.md +29 -0
- package/protocols/security.md +29 -0
- package/schemas/event.schema.json +86 -0
- package/schemas/evidence.schema.json +94 -0
- package/schemas/feature-decomposition.schema.json +48 -0
- package/schemas/feature-plan.schema.json +95 -0
- package/schemas/goal-graph.schema.json +61 -0
- package/schemas/integration.schema.json +119 -0
- package/schemas/orchestration.schema.json +69 -0
- package/schemas/project.schema.json +209 -0
- package/schemas/providers.schema.json +142 -0
- package/schemas/quality.schema.json +50 -0
- package/schemas/work-action.schema.json +44 -0
- package/schemas/work-request.schema.json +253 -0
- package/src/adapters/confluence.js +138 -0
- package/src/adapters/contract.js +657 -0
- package/src/adapters/factory.js +122 -0
- package/src/adapters/figma.js +178 -0
- package/src/adapters/fixtures.js +183 -0
- package/src/adapters/github.js +314 -0
- package/src/adapters/http.js +704 -0
- package/src/adapters/jira.js +195 -0
- package/src/adapters/linear.js +171 -0
- package/src/adapters/node-transport.js +59 -0
- package/src/cli/integration-setup-prompt.js +32 -0
- package/src/cli/interrupt.js +25 -0
- package/src/cli/main.js +596 -0
- package/src/cli/output.js +174 -0
- package/src/cli/parse-args.js +246 -0
- package/src/cli/project-discovery.js +70 -0
- package/src/cli/task-confirmation.js +38 -0
- package/src/cli/task-presentation.js +22 -0
- package/src/clients/claude.js +214 -0
- package/src/clients/codex.js +180 -0
- package/src/clients/compatibility.js +23 -0
- package/src/clients/contract.js +224 -0
- package/src/clients/fake.js +170 -0
- package/src/clients/process-runner.js +657 -0
- package/src/clients/result-contract.js +78 -0
- package/src/commands/delivery-publish.js +80 -0
- package/src/commands/delivery-remote.js +230 -0
- package/src/commands/delivery-review-update.js +39 -0
- package/src/commands/delivery-tracker.js +94 -0
- package/src/commands/delivery-transition.js +79 -0
- package/src/commands/delivery.js +223 -0
- package/src/commands/dependency-approval.js +21 -0
- package/src/commands/doctor.js +191 -0
- package/src/commands/evidence.js +44 -0
- package/src/commands/feature.js +212 -0
- package/src/commands/goals.js +231 -0
- package/src/commands/human-run.js +208 -0
- package/src/commands/human-task.js +278 -0
- package/src/commands/init.js +848 -0
- package/src/commands/install.js +814 -0
- package/src/commands/integration-setup.js +65 -0
- package/src/commands/integrations.js +54 -0
- package/src/commands/models.js +158 -0
- package/src/commands/orchestrate.js +61 -0
- package/src/commands/preflight.js +125 -0
- package/src/commands/protocols.js +228 -0
- package/src/commands/repositories.js +77 -0
- package/src/commands/setup-remote.js +41 -0
- package/src/commands/setup.js +214 -0
- package/src/commands/status.js +224 -0
- package/src/commands/support.js +44 -0
- package/src/commands/task-completion.js +104 -0
- package/src/commands/uninstall.js +234 -0
- package/src/commands/verify.js +171 -0
- package/src/commands/work.js +220 -0
- package/src/commands/worktrees.js +52 -0
- package/src/config/command-readiness.js +218 -0
- package/src/config/commands.js +206 -0
- package/src/config/defaults.js +55 -0
- package/src/config/load.js +230 -0
- package/src/config/validate.js +560 -0
- package/src/delivery/bitbucket-review.js +123 -0
- package/src/delivery/branch-publication.js +23 -0
- package/src/delivery/contract.js +379 -0
- package/src/delivery/github-deployment.js +132 -0
- package/src/delivery/github-review.js +5 -0
- package/src/delivery/github.js +382 -0
- package/src/delivery/gitlab-review.js +5 -0
- package/src/delivery/gitlab.js +395 -0
- package/src/delivery/prepare.js +90 -0
- package/src/delivery/publication-process.js +33 -0
- package/src/delivery/publication-transport.js +112 -0
- package/src/delivery/review-request.js +184 -0
- package/src/delivery/review-update.js +74 -0
- package/src/delivery/service.js +400 -0
- package/src/delivery/store.js +76 -0
- package/src/delivery/tracker-target.js +51 -0
- package/src/delivery/tracker-transition.js +158 -0
- package/src/delivery/tracker.js +164 -0
- package/src/discovery/git.js +85 -0
- package/src/discovery/portable.js +108 -0
- package/src/discovery/project.js +345 -0
- package/src/discovery/tools.js +145 -0
- package/src/evaluations/approval.js +20 -0
- package/src/evaluations/cost-policy.js +20 -0
- package/src/evaluations/harness-attempt.js +113 -0
- package/src/evaluations/live-runner.js +55 -0
- package/src/evaluations/profile.js +28 -0
- package/src/evaluations/report.js +11 -0
- package/src/evaluations/text-attempt.js +18 -0
- package/src/evidence/checksum.js +87 -0
- package/src/evidence/collect.js +727 -0
- package/src/evidence/validate.js +354 -0
- package/src/feature/accepted-integration.js +46 -0
- package/src/feature/actions.js +37 -0
- package/src/feature/client-profile.js +56 -0
- package/src/feature/decomposition-contract.js +58 -0
- package/src/feature/host-execution.js +633 -0
- package/src/feature/host-lock-recovery.js +96 -0
- package/src/feature/host-run-lock.js +32 -0
- package/src/feature/local-approval.js +183 -0
- package/src/feature/plan-contract.js +215 -0
- package/src/feature/planner.js +253 -0
- package/src/feature/run-store.js +241 -0
- package/src/feature/runtime-bridge.js +840 -0
- package/src/feature/verification-report.js +168 -0
- package/src/feature/workflow.js +375 -0
- package/src/git/client.js +509 -0
- package/src/git/integration-worktree.js +153 -0
- package/src/git/reconcile.js +222 -0
- package/src/git/reservations.js +450 -0
- package/src/git/worktrees.js +478 -0
- package/src/graph/completion.js +189 -0
- package/src/graph/fixtures.js +116 -0
- package/src/graph/reducer.js +124 -0
- package/src/graph/scheduler.js +252 -0
- package/src/graph/validate.js +106 -0
- package/src/install/managed.js +579 -0
- package/src/install/project-reference.js +33 -0
- package/src/install/project-runtime.js +142 -0
- package/src/install/runtime-integrity.cjs +141 -0
- package/src/integrations/capabilities.js +69 -0
- package/src/integrations/host-observation.js +26 -0
- package/src/integrations/registry.js +56 -0
- package/src/models/delegate.js +102 -0
- package/src/models/profiles.js +53 -0
- package/src/models/protocols.js +208 -0
- package/src/models/registry.js +83 -0
- package/src/models/transport.js +59 -0
- package/src/policy/approvals.js +251 -0
- package/src/policy/authority.js +282 -0
- package/src/policy/budget.js +149 -0
- package/src/policy/command-bootstrap.js +219 -0
- package/src/policy/commands.js +803 -0
- package/src/prompts/launch-contract.js +43 -0
- package/src/prompts/planning-contract.js +92 -0
- package/src/protocols/presentation.js +53 -0
- package/src/protocols/project.js +289 -0
- package/src/quality/runner.js +497 -0
- package/src/quality/traceability.js +215 -0
- package/src/repositories/identity.js +59 -0
- package/src/repositories/index.js +2 -0
- package/src/repositories/provider.js +210 -0
- package/src/runtime/application.js +353 -0
- package/src/runtime/dependency-directory.js +33 -0
- package/src/runtime/harness-discovery.js +121 -0
- package/src/runtime/heartbeat.js +47 -0
- package/src/runtime/instance-store.js +112 -0
- package/src/runtime/orchestrator.js +1133 -0
- package/src/runtime/portable-dependencies.js +94 -0
- package/src/runtime/recovery.js +190 -0
- package/src/runtime/retry.js +54 -0
- package/src/runtime/supervisor.js +166 -0
- package/src/runtime/worktree-bootstrap.js +169 -0
- package/src/state/event-store.js +333 -0
- package/src/state/lock.js +266 -0
- package/src/state/paths.js +274 -0
- package/src/state/redact.js +106 -0
- package/src/state/snapshot-store.js +310 -0
- package/src/status/public/app.js +96 -0
- package/src/status/public/index.html +37 -0
- package/src/status/public/styles.css +50 -0
- package/src/status/server.js +323 -0
- package/src/status/view-model.js +356 -0
- package/src/support/bundle.js +195 -0
- package/src/support/failure-report.js +96 -0
- package/src/work-request/contract.js +250 -0
- package/src/work-request/host.js +78 -0
- package/src/work-request/local.js +163 -0
- package/src/work-request/tracker.js +105 -0
- package/templates/evidence/qa-bundle.json +194 -0
- package/templates/github-actions/rivet-deploy.yml +98 -0
- package/templates/harness/SKILL.md +90 -0
- package/templates/project/.rivet/orchestration.yaml +42 -0
- package/templates/project/.rivet/project.yaml +14 -0
- package/templates/project/.rivet/providers.yaml +8 -0
- package/templates/project/.rivet/quality.yaml +16 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join, resolve, sep } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { validateEvidence as validateCanonicalEvidence } from '../config/validate.js';
|
|
5
|
+
import { sha256 } from './checksum.js';
|
|
6
|
+
|
|
7
|
+
const SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
8
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
9
|
+
const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
10
|
+
const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
11
|
+
|
|
12
|
+
function record(value) { return value && typeof value === 'object' && !Array.isArray(value); }
|
|
13
|
+
function array(value, maximum) { return Array.isArray(value) && value.length <= maximum; }
|
|
14
|
+
function id(value) { return typeof value === 'string' && value.length <= 64 && ID.test(value); }
|
|
15
|
+
function sha(value) { return typeof value === 'string' && SHA.test(value); }
|
|
16
|
+
function checksum(value) { return typeof value === 'string' && SHA256.test(value); }
|
|
17
|
+
function timestamp(value) {
|
|
18
|
+
return typeof value === 'string' && ISO.test(value)
|
|
19
|
+
&& Number.isSafeInteger(Date.parse(value)) && new Date(Date.parse(value)).toISOString() === value;
|
|
20
|
+
}
|
|
21
|
+
function httpsUrl(value) {
|
|
22
|
+
if (typeof value !== 'string' || value.length > 2_048) return false;
|
|
23
|
+
try {
|
|
24
|
+
const parsed = new URL(value);
|
|
25
|
+
return parsed.protocol === 'https:' && !parsed.username && !parsed.password && !parsed.search && !parsed.hash;
|
|
26
|
+
} catch { return false; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function captureOptions(value) {
|
|
30
|
+
if (!record(value)) return null;
|
|
31
|
+
let keys;
|
|
32
|
+
try { keys = Reflect.ownKeys(value); } catch { return null; }
|
|
33
|
+
if (keys.some(key => typeof key !== 'string' || key !== 'runDirectory')) return null;
|
|
34
|
+
const output = {};
|
|
35
|
+
for (const key of keys) {
|
|
36
|
+
let descriptor;
|
|
37
|
+
try { descriptor = Object.getOwnPropertyDescriptor(value, key); } catch { return null; }
|
|
38
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) return null;
|
|
39
|
+
output[key] = descriptor.value;
|
|
40
|
+
}
|
|
41
|
+
return output;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sameIdentity(left, right) { return left.dev === right.dev && left.ino === right.ino; }
|
|
45
|
+
|
|
46
|
+
function persistedFile(root, relativePath, maximumBytes) {
|
|
47
|
+
if (typeof relativePath !== 'string' || !relativePath || isAbsolute(relativePath)
|
|
48
|
+
|| relativePath.includes('\\') || relativePath.split('/').some(part => !part || part === '.' || part === '..')) {
|
|
49
|
+
throw new Error('invalid persisted path');
|
|
50
|
+
}
|
|
51
|
+
const absolute = resolve(root, relativePath);
|
|
52
|
+
if (!absolute.startsWith(root + sep)) throw new Error('invalid persisted path');
|
|
53
|
+
let cursor = root;
|
|
54
|
+
for (const part of relativePath.split('/').slice(0, -1)) {
|
|
55
|
+
cursor = join(cursor, part);
|
|
56
|
+
const directory = lstatSync(cursor, { bigint: true });
|
|
57
|
+
if (!directory.isDirectory() || directory.isSymbolicLink()) throw new Error('unsafe persisted directory');
|
|
58
|
+
}
|
|
59
|
+
const before = lstatSync(absolute, { bigint: true });
|
|
60
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n
|
|
61
|
+
|| before.size < 0n || before.size > BigInt(maximumBytes)) throw new Error('unsafe persisted file');
|
|
62
|
+
let descriptor;
|
|
63
|
+
try {
|
|
64
|
+
descriptor = openSync(absolute, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
65
|
+
const opened = fstatSync(descriptor, { bigint: true });
|
|
66
|
+
if (!opened.isFile() || opened.nlink !== 1n || !sameIdentity(before, opened)) throw new Error('persisted file changed');
|
|
67
|
+
const bytes = readFileSync(descriptor);
|
|
68
|
+
const after = fstatSync(descriptor, { bigint: true });
|
|
69
|
+
if (!sameIdentity(opened, after) || after.size !== BigInt(bytes.length)) throw new Error('persisted file changed');
|
|
70
|
+
const current = lstatSync(absolute, { bigint: true });
|
|
71
|
+
if (!sameIdentity(after, current) || current.isSymbolicLink()) throw new Error('persisted file changed');
|
|
72
|
+
return bytes;
|
|
73
|
+
} finally { if (descriptor !== undefined) closeSync(descriptor); }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validatePersisted(manifest, runDirectory, errors) {
|
|
77
|
+
if (typeof runDirectory !== 'string' || !isAbsolute(runDirectory) || resolve(runDirectory) !== runDirectory) {
|
|
78
|
+
errors.push('Persisted evidence validation context is invalid.');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const before = lstatSync(runDirectory, { bigint: true });
|
|
82
|
+
const canonical = realpathSync(runDirectory);
|
|
83
|
+
const after = lstatSync(runDirectory, { bigint: true });
|
|
84
|
+
if (!before.isDirectory() || before.isSymbolicLink() || !sameIdentity(before, after) || canonical !== runDirectory) {
|
|
85
|
+
errors.push('Persisted evidence directory identity is invalid.');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const rootEntries = readdirSync(runDirectory, { withFileTypes: true });
|
|
89
|
+
if (rootEntries.length < 3 || rootEntries.length > 4
|
|
90
|
+
|| rootEntries.some(entry => entry.isSymbolicLink())) {
|
|
91
|
+
errors.push('Persisted evidence directory contains an unexpected entry.');
|
|
92
|
+
}
|
|
93
|
+
const rootByName = new Map(rootEntries.map(entry => [entry.name, entry]));
|
|
94
|
+
const publicationExists = rootByName.has('publication.json');
|
|
95
|
+
const expectedRootNames = new Set(['artifacts', 'manifest.json', 'qa-bundle.json', ...(publicationExists ? ['publication.json'] : [])]);
|
|
96
|
+
if (rootByName.size !== rootEntries.length || rootByName.size !== expectedRootNames.size
|
|
97
|
+
|| [...rootByName.keys()].some(name => !expectedRootNames.has(name))
|
|
98
|
+
|| !rootByName.get('artifacts')?.isDirectory()
|
|
99
|
+
|| !rootByName.get('manifest.json')?.isFile() || !rootByName.get('qa-bundle.json')?.isFile()
|
|
100
|
+
|| (publicationExists && !rootByName.get('publication.json')?.isFile())) {
|
|
101
|
+
errors.push('Persisted evidence directory file set is invalid.');
|
|
102
|
+
}
|
|
103
|
+
const artifactNames = manifest.artifacts.map(artifact => {
|
|
104
|
+
const parts = artifact.path.split('/');
|
|
105
|
+
if (parts.length !== 2 || parts[0] !== 'artifacts') throw new Error('invalid artifact path');
|
|
106
|
+
return parts[1];
|
|
107
|
+
});
|
|
108
|
+
const artifactEntries = readdirSync(join(runDirectory, 'artifacts'), { withFileTypes: true });
|
|
109
|
+
const expectedArtifactNames = new Set(artifactNames);
|
|
110
|
+
if (expectedArtifactNames.size !== artifactNames.length || artifactEntries.length !== expectedArtifactNames.size
|
|
111
|
+
|| artifactEntries.some(entry => entry.isSymbolicLink() || !entry.isFile() || !expectedArtifactNames.has(entry.name))) {
|
|
112
|
+
errors.push('Persisted artifact directory file set is invalid.');
|
|
113
|
+
}
|
|
114
|
+
const expectedManifest = JSON.stringify(manifest, null, 2) + '\n';
|
|
115
|
+
const manifestBytes = persistedFile(runDirectory, 'manifest.json', 8 * 1024 * 1024);
|
|
116
|
+
if (!manifestBytes.equals(Buffer.from(expectedManifest))) errors.push('Persisted manifest bytes do not match the validated manifest.');
|
|
117
|
+
const archiveBytes = persistedFile(runDirectory, 'qa-bundle.json', 128 * 1024 * 1024);
|
|
118
|
+
let archive;
|
|
119
|
+
try { archive = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(archiveBytes)); }
|
|
120
|
+
catch { errors.push('Persisted archive is invalid.'); return; }
|
|
121
|
+
if (!record(archive) || archive.schemaVersion !== 1 || archive.runId !== manifest.runId
|
|
122
|
+
|| archive.commitSha !== manifest.commitSha || !array(archive.files, 257)) {
|
|
123
|
+
errors.push('Persisted archive identity is invalid.');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const canonicalFiles = [];
|
|
127
|
+
for (const artifact of manifest.artifacts) {
|
|
128
|
+
const bytes = persistedFile(runDirectory, artifact.path, 128 * 1024 * 1024);
|
|
129
|
+
const actualChecksum = sha256(bytes);
|
|
130
|
+
if (artifact.sha256 !== actualChecksum || artifact.bytes !== bytes.length) {
|
|
131
|
+
errors.push('Persisted evidence file checksum is invalid.');
|
|
132
|
+
}
|
|
133
|
+
canonicalFiles.push({
|
|
134
|
+
path: artifact.path, sha256: actualChecksum, bytes: bytes.length, contentBase64: bytes.toString('base64'),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
canonicalFiles.push({
|
|
138
|
+
path: 'manifest.json', sha256: sha256(manifestBytes), bytes: manifestBytes.length,
|
|
139
|
+
contentBase64: manifestBytes.toString('base64'),
|
|
140
|
+
});
|
|
141
|
+
canonicalFiles.sort((left, right) => left.path.localeCompare(right.path));
|
|
142
|
+
const canonicalArchive = JSON.stringify({
|
|
143
|
+
schemaVersion: 1, runId: manifest.runId, commitSha: manifest.commitSha, files: canonicalFiles,
|
|
144
|
+
}) + '\n';
|
|
145
|
+
if (!archiveBytes.equals(Buffer.from(canonicalArchive))) {
|
|
146
|
+
errors.push('Persisted archive bytes do not match the canonical evidence reconstruction.');
|
|
147
|
+
}
|
|
148
|
+
if (publicationExists) {
|
|
149
|
+
const publicationBytes = persistedFile(runDirectory, 'publication.json', 64 * 1024);
|
|
150
|
+
let publication;
|
|
151
|
+
try { publication = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(publicationBytes)); }
|
|
152
|
+
catch { errors.push('Persisted publication attestation is invalid.'); return; }
|
|
153
|
+
const keys = record(publication) ? Object.keys(publication).sort() : [];
|
|
154
|
+
if (!record(publication)
|
|
155
|
+
|| keys.join(',') !== 'approvalReceiptId,approverId,approverPrincipal,checksum,remoteUrl,status'
|
|
156
|
+
|| publication.status !== 'published' || !httpsUrl(publication.remoteUrl)
|
|
157
|
+
|| publication.checksum !== sha256(archiveBytes) || !id(publication.approvalReceiptId)
|
|
158
|
+
|| !id(publication.approverId) || publication.approverPrincipal !== 'human'
|
|
159
|
+
|| !publicationBytes.equals(Buffer.from(JSON.stringify(publication, null, 2) + '\n'))) {
|
|
160
|
+
errors.push('Persisted publication is not bound to the exact archive checksum and approval.');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const final = lstatSync(runDirectory, { bigint: true });
|
|
164
|
+
if (!sameIdentity(before, final) || final.isSymbolicLink() || realpathSync(runDirectory) !== runDirectory) {
|
|
165
|
+
errors.push('Persisted evidence directory changed during validation.');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function validateGate(gate, manifest, errors) {
|
|
170
|
+
if (!record(gate) || !id(gate.id) || !['passed', 'failed'].includes(gate.status)
|
|
171
|
+
|| typeof gate.required !== 'boolean' || gate.commitSha !== manifest.commitSha
|
|
172
|
+
|| !timestamp(gate.startedAt) || !timestamp(gate.endedAt) || Date.parse(gate.endedAt) < Date.parse(gate.startedAt)
|
|
173
|
+
|| typeof gate.cwd !== 'string' || !record(gate.command) || typeof gate.command.executable !== 'string'
|
|
174
|
+
|| !Array.isArray(gate.command.args) || gate.command.args.some(arg => typeof arg !== 'string')
|
|
175
|
+
|| !Number.isInteger(gate.exitCode) || !record(gate.output) || !array(gate.artifacts, 256)
|
|
176
|
+
|| gate.artifacts.some(artifact => !record(artifact) || typeof artifact.path !== 'string'
|
|
177
|
+
|| !checksum(artifact.sha256) || !Number.isSafeInteger(artifact.bytes) || artifact.bytes < 0)) {
|
|
178
|
+
errors.push('A quality gate lacks deterministic command provenance.');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if ((gate.status === 'passed' && (gate.exitCode !== 0 || gate.executionStatus !== 'success'))
|
|
182
|
+
|| (gate.status === 'failed' && gate.exitCode === 0 && gate.executionStatus === 'success')) {
|
|
183
|
+
errors.push('Quality gate status contradicts its deterministic exit provenance.');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function validateEvidenceManifest(manifest, options = {}) {
|
|
188
|
+
const errors = [];
|
|
189
|
+
try {
|
|
190
|
+
const configured = captureOptions(options);
|
|
191
|
+
if (!configured) return { valid: false, errors: ['Evidence validation context is invalid.'] };
|
|
192
|
+
if (!record(manifest) || manifest.schemaVersion !== 1 || !id(manifest.runId)
|
|
193
|
+
|| manifest.status !== 'complete' || !sha(manifest.commitSha) || !timestamp(manifest.createdAt)
|
|
194
|
+
|| !checksum(manifest.preapprovalContentHash)) {
|
|
195
|
+
return { valid: false, errors: ['Manifest identity or commit provenance is invalid.'] };
|
|
196
|
+
}
|
|
197
|
+
if (!array(manifest.gates, 64) || manifest.gates.length === 0) errors.push('At least one quality gate is required.');
|
|
198
|
+
else {
|
|
199
|
+
for (const gate of manifest.gates) validateGate(gate, manifest, errors);
|
|
200
|
+
if (new Set(manifest.gates.map(gate => String(gate?.id).toLowerCase())).size !== manifest.gates.length) {
|
|
201
|
+
errors.push('Quality gate identifiers must be unique.');
|
|
202
|
+
}
|
|
203
|
+
if (manifest.gates.some(gate => gate.required && gate.status !== 'passed')) errors.push('Every required quality gate must pass.');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const traceability = manifest.traceability;
|
|
207
|
+
if (!record(traceability) || traceability.valid !== true || !array(traceability.coverage, 10_000)
|
|
208
|
+
|| traceability.coverage.length === 0 || !array(traceability.errors, 10_000) || traceability.errors.length !== 0) {
|
|
209
|
+
errors.push('Acceptance traceability is incomplete.');
|
|
210
|
+
}
|
|
211
|
+
if (!array(manifest.contexts, 10_000) || manifest.contexts.length === 0) {
|
|
212
|
+
errors.push('Route, persona, viewport, Figma version, and preview context are required.');
|
|
213
|
+
} else {
|
|
214
|
+
for (const context of manifest.contexts) {
|
|
215
|
+
if (!record(context) || !id(context.testId)
|
|
216
|
+
|| typeof context.route !== 'string' || !context.route.startsWith('/') || context.route.length > 500
|
|
217
|
+
|| !id(context.persona) || !record(context.viewport)
|
|
218
|
+
|| !Number.isSafeInteger(context.viewport.width) || context.viewport.width < 200 || context.viewport.width > 10_000
|
|
219
|
+
|| !Number.isSafeInteger(context.viewport.height) || context.viewport.height < 200 || context.viewport.height > 10_000
|
|
220
|
+
|| typeof context.figmaVersion !== 'string' || context.figmaVersion.length < 1 || context.figmaVersion.length > 200
|
|
221
|
+
|| !httpsUrl(context.previewUrl)) {
|
|
222
|
+
errors.push('Evidence execution context is invalid.');
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const contextTestIds = new Set(array(manifest.contexts, 10_000) ? manifest.contexts.filter(record).map(context => context.testId) : []);
|
|
229
|
+
const gateIds = new Set(array(manifest.gates, 64) ? manifest.gates.filter(record).map(gate => gate.id) : []);
|
|
230
|
+
const passedGateIds = new Set(array(manifest.gates, 64)
|
|
231
|
+
? manifest.gates.filter(gate => record(gate) && gate.status === 'passed').map(gate => gate.id) : []);
|
|
232
|
+
if (record(traceability) && array(traceability.coverage, 10_000)) {
|
|
233
|
+
for (const coverage of traceability.coverage) {
|
|
234
|
+
if (!record(coverage) || !['test', 'manual-review'].includes(coverage.method) || !id(coverage.evidenceId)
|
|
235
|
+
|| (coverage.method === 'test' && (!contextTestIds.has(coverage.evidenceId)
|
|
236
|
+
|| !gateIds.has(coverage.gateId) || !passedGateIds.has(coverage.gateId)
|
|
237
|
+
|| typeof coverage.resultPath !== 'string' || !checksum(coverage.resultSha256)
|
|
238
|
+
|| !manifest.gates.some(gate => gate.id === coverage.gateId
|
|
239
|
+
&& gate.artifacts.some(artifact => artifact.path === coverage.resultPath
|
|
240
|
+
&& artifact.sha256 === coverage.resultSha256))
|
|
241
|
+
|| !manifest.artifacts.some(artifact => artifact.sourcePath === coverage.resultPath
|
|
242
|
+
&& artifact.sha256 === coverage.resultSha256)))) {
|
|
243
|
+
errors.push('Traceability evidence is not linked to its authenticated gate result and execution context.');
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!array(manifest.artifacts, 256) || manifest.artifacts.length === 0) errors.push('Evidence artifacts are required.');
|
|
250
|
+
else {
|
|
251
|
+
const types = new Set();
|
|
252
|
+
for (const artifact of manifest.artifacts) {
|
|
253
|
+
if (!record(artifact) || !id(artifact.id) || !id(artifact.testId)
|
|
254
|
+
|| !['screenshot', 'playwright-report', 'trace', 'report'].includes(artifact.type)
|
|
255
|
+
|| typeof artifact.sourcePath !== 'string' || isAbsolute(artifact.sourcePath)
|
|
256
|
+
|| artifact.sourcePath.includes('\\') || artifact.sourcePath.split('/').some(part => !part || part === '.' || part === '..')
|
|
257
|
+
|| typeof artifact.path !== 'string' || !artifact.path.startsWith('artifacts/')
|
|
258
|
+
|| !checksum(artifact.sha256) || !Number.isSafeInteger(artifact.bytes)
|
|
259
|
+
|| artifact.bytes < 0 || artifact.bytes > 128 * 1024 * 1024 || !contextTestIds.has(artifact.testId)
|
|
260
|
+
|| (artifact.type === 'screenshot' ? artifact.mediaType !== 'image/png' : artifact.mediaType !== undefined)) {
|
|
261
|
+
errors.push('Evidence artifact metadata, checksum, or test binding is invalid.');
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
const coverage = manifest.traceability.coverage.find(entry => entry.method === 'test' && entry.evidenceId === artifact.testId);
|
|
265
|
+
const gate = manifest.gates.find(entry => entry.id === coverage?.gateId && entry.status === 'passed');
|
|
266
|
+
if (!gate?.artifacts.some(entry => entry.path === artifact.sourcePath
|
|
267
|
+
&& entry.sha256 === artifact.sha256 && entry.bytes === artifact.bytes)) {
|
|
268
|
+
errors.push('Evidence artifact is not bound to an authenticated gate artifact.');
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
types.add(artifact.type);
|
|
272
|
+
}
|
|
273
|
+
if (new Set(manifest.artifacts.map(artifact => String(artifact?.id).toLowerCase())).size !== manifest.artifacts.length
|
|
274
|
+
|| new Set(manifest.artifacts.map(artifact => String(artifact?.path).toLowerCase())).size !== manifest.artifacts.length) {
|
|
275
|
+
errors.push('Evidence artifact identifiers and paths must be unique.');
|
|
276
|
+
}
|
|
277
|
+
if (!types.has('screenshot')) errors.push('A screenshot is required.');
|
|
278
|
+
if (!types.has('playwright-report')) errors.push('A Playwright report is required.');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!array(manifest.reviews, 256) || manifest.reviews.length === 0
|
|
282
|
+
|| !manifest.reviews.some(review => record(review) && review.required === true)
|
|
283
|
+
|| manifest.reviews.some(review => !record(review) || !id(review.id) || !id(review.reviewerId)
|
|
284
|
+
|| typeof review.required !== 'boolean' || review.status !== 'approved'
|
|
285
|
+
|| !id(review.approvalReceiptId) || !['human', 'agent'].includes(review.approverPrincipal)
|
|
286
|
+
|| !checksum(review.contentHash))) errors.push('Every required review must have an authenticated content binding.');
|
|
287
|
+
if (!record(manifest.finalApproval) || !id(manifest.finalApproval.approvalReceiptId)
|
|
288
|
+
|| !id(manifest.finalApproval.approverId) || manifest.finalApproval.approverPrincipal !== 'human'
|
|
289
|
+
|| manifest.finalApproval.decision !== 'approved' || !checksum(manifest.finalApproval.contentHash)) {
|
|
290
|
+
errors.push('Exact content-bound human final approval is required.');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const core = {
|
|
294
|
+
schemaVersion: 1, runId: manifest.runId, graphId: manifest.evidence?.graphId, commitSha: manifest.commitSha,
|
|
295
|
+
gates: manifest.gates, traceability: manifest.traceability, contexts: manifest.contexts, artifacts: manifest.artifacts,
|
|
296
|
+
};
|
|
297
|
+
const reviewContentHash = sha256(JSON.stringify(core));
|
|
298
|
+
if (manifest.reviews.some(review => review.contentHash !== reviewContentHash)) {
|
|
299
|
+
errors.push('Review approvals are not bound to the canonical evidence content.');
|
|
300
|
+
}
|
|
301
|
+
const preapprovalContentHash = sha256(JSON.stringify({ core, reviews: manifest.reviews }));
|
|
302
|
+
if (manifest.preapprovalContentHash !== preapprovalContentHash
|
|
303
|
+
|| manifest.finalApproval?.contentHash !== preapprovalContentHash) {
|
|
304
|
+
errors.push('Final approval is not bound to the canonical preapproval content.');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
validateCanonicalEvidence(manifest.evidence);
|
|
309
|
+
const evidenceItems = new Map(manifest.evidence.items.map(item => [item.id, item]));
|
|
310
|
+
const commitItem = manifest.evidence.items.find(item => item.type === 'commit');
|
|
311
|
+
if (manifest.evidence.id !== manifest.runId || !commitItem || commitItem.source.commit !== manifest.commitSha
|
|
312
|
+
|| manifest.evidence.publicationState !== (manifest.durablyPublished ? 'approved' : 'draft')) {
|
|
313
|
+
errors.push('Canonical evidence ledger is not bound to this manifest.');
|
|
314
|
+
}
|
|
315
|
+
for (const artifact of manifest.artifacts ?? []) {
|
|
316
|
+
const item = evidenceItems.get(artifact.id);
|
|
317
|
+
const expectedType = artifact.type === 'screenshot' ? 'screenshot' : 'report';
|
|
318
|
+
if (!item || item.type !== expectedType || item.location !== artifact.path || item.checksum !== 'sha256:' + artifact.sha256) {
|
|
319
|
+
errors.push('Artifact checksum is not bound to the canonical evidence ledger.');
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
for (const review of manifest.reviews ?? []) {
|
|
324
|
+
const item = evidenceItems.get(review.id);
|
|
325
|
+
if (!item || item.type !== 'review' || item.approvalState !== review.status) {
|
|
326
|
+
errors.push('Review is not bound to the canonical evidence ledger.');
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const approvalItem = evidenceItems.get(manifest.finalApproval?.approvalReceiptId);
|
|
331
|
+
if (!approvalItem || approvalItem.type !== 'human-approval'
|
|
332
|
+
|| approvalItem.producer.id !== manifest.finalApproval.approverId
|
|
333
|
+
|| approvalItem.approval.actorId !== manifest.finalApproval.approverId
|
|
334
|
+
|| approvalItem.approval.decision !== 'approved') errors.push('Final approval is not bound to the canonical evidence ledger.');
|
|
335
|
+
for (const coverage of manifest.traceability?.coverage ?? []) {
|
|
336
|
+
const item = evidenceItems.get(coverage.evidenceId);
|
|
337
|
+
if (!item || (coverage.method === 'test' && (item.type !== 'test' || item.details.status !== 'passed'))
|
|
338
|
+
|| (coverage.method === 'manual-review' && item.type !== 'review')) {
|
|
339
|
+
errors.push('Traceability is not bound to the canonical evidence ledger.');
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} catch { errors.push('Canonical evidence ledger is invalid.'); }
|
|
344
|
+
|
|
345
|
+
if (!record(manifest.publication) || typeof manifest.durablyPublished !== 'boolean') errors.push('Publication state is invalid.');
|
|
346
|
+
else if (manifest.durablyPublished || manifest.publication.status !== 'local-only') {
|
|
347
|
+
errors.push('The immutable archive manifest must remain local-only; publication is an external checksum attestation.');
|
|
348
|
+
}
|
|
349
|
+
if (Object.hasOwn(configured, 'runDirectory')) validatePersisted(manifest, configured.runDirectory, errors);
|
|
350
|
+
} catch {
|
|
351
|
+
errors.push('Manifest validation failed safely.');
|
|
352
|
+
}
|
|
353
|
+
return { valid: errors.length === 0, errors: [...new Set(errors)] };
|
|
354
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { immutableJson } from '../clients/contract.js';
|
|
4
|
+
import { assertResolvedStatePaths } from '../state/paths.js';
|
|
5
|
+
import { createSnapshotStore, readSnapshotWithoutLock } from '../state/snapshot-store.js';
|
|
6
|
+
|
|
7
|
+
const SHA = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
8
|
+
const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
9
|
+
|
|
10
|
+
function accepted(value) {
|
|
11
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
12
|
+
|| value.schemaVersion !== 1 || !ID.test(value.runId)
|
|
13
|
+
|| !SHA.test(value.baselineCommit) || !SHA.test(value.commitSha)
|
|
14
|
+
|| !Number.isSafeInteger(value.runtimeVersion) || value.runtimeVersion < 1
|
|
15
|
+
|| typeof value.path !== 'string' || value.path.length > 4096
|
|
16
|
+
|| !isAbsolute(value.path) || resolve(value.path) !== value.path
|
|
17
|
+
|| typeof value.branch !== 'string' || value.branch.length > 256) {
|
|
18
|
+
throw new TypeError('Accepted integration identity is invalid');
|
|
19
|
+
}
|
|
20
|
+
return immutableJson({
|
|
21
|
+
schemaVersion: 1,
|
|
22
|
+
runId: value.runId,
|
|
23
|
+
baselineCommit: value.baselineCommit,
|
|
24
|
+
commitSha: value.commitSha,
|
|
25
|
+
runtimeVersion: value.runtimeVersion,
|
|
26
|
+
path: value.path,
|
|
27
|
+
branch: value.branch,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createAcceptedIntegrationStore(paths) {
|
|
32
|
+
assertResolvedStatePaths(paths);
|
|
33
|
+
const snapshots = createSnapshotStore(paths, { environment: {} });
|
|
34
|
+
return Object.freeze({
|
|
35
|
+
async readOnly() {
|
|
36
|
+
const snapshot = await readSnapshotWithoutLock(paths);
|
|
37
|
+
return snapshot === null ? null : immutableJson({ ...accepted(snapshot.data), version: snapshot.version });
|
|
38
|
+
},
|
|
39
|
+
async write(value) {
|
|
40
|
+
const data = accepted(value);
|
|
41
|
+
const version = await snapshots.version();
|
|
42
|
+
const stored = await snapshots.write(data, { expectedVersion: version, environment: {} });
|
|
43
|
+
return immutableJson({ ...accepted(stored.data), version: stored.version });
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import Ajv from 'ajv';
|
|
4
|
+
|
|
5
|
+
import { immutableJson } from '../clients/contract.js';
|
|
6
|
+
import { validateLaunchPayload } from '../prompts/launch-contract.js';
|
|
7
|
+
|
|
8
|
+
export const WORK_ACTION_KIND = 'agilno.work-action';
|
|
9
|
+
|
|
10
|
+
const schema = JSON.parse(readFileSync(new URL('../../schemas/work-action.schema.json', import.meta.url), 'utf8'));
|
|
11
|
+
const validateSchema = new Ajv({ allErrors: true, strict: true }).compile(schema);
|
|
12
|
+
|
|
13
|
+
export class WorkActionError extends Error {
|
|
14
|
+
constructor() {
|
|
15
|
+
super('Host work action is invalid.');
|
|
16
|
+
this.name = 'WorkActionError';
|
|
17
|
+
this.code = 'ERR_INVALID_WORK_ACTION';
|
|
18
|
+
this.safeMessage = this.message;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function fail() { throw new WorkActionError(); }
|
|
23
|
+
|
|
24
|
+
export function createWorkAction(input) {
|
|
25
|
+
return validateWorkAction({ schemaVersion: 1, kind: WORK_ACTION_KIND, ...input });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function validateWorkAction(input) {
|
|
29
|
+
let captured;
|
|
30
|
+
try { captured = immutableJson(input); } catch { fail(); }
|
|
31
|
+
if (!validateSchema(captured)) fail();
|
|
32
|
+
let contract;
|
|
33
|
+
try { contract = validateLaunchPayload(captured.payload); } catch { fail(); }
|
|
34
|
+
if (contract.nodeId !== captured.nodeId
|
|
35
|
+
|| contract.worktree.reservationId !== captured.reservationId) fail();
|
|
36
|
+
return captured;
|
|
37
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const FEATURE_PLANNING_TIMEOUT_MS = 120_000;
|
|
2
|
+
|
|
3
|
+
export const CLAUDE_FEATURE_PROFILE = Object.freeze({
|
|
4
|
+
schemaVersion: 1,
|
|
5
|
+
id: 'claude-bounded-sonnet-v1',
|
|
6
|
+
provider: 'claude',
|
|
7
|
+
model: 'sonnet',
|
|
8
|
+
planning: Object.freeze({
|
|
9
|
+
effort: 'low',
|
|
10
|
+
timeoutMs: FEATURE_PLANNING_TIMEOUT_MS,
|
|
11
|
+
maxCostUsd: 1,
|
|
12
|
+
}),
|
|
13
|
+
execution: Object.freeze({ maxCostUsd: 2 }),
|
|
14
|
+
fallbackModel: null,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function exactObject(value, expected) {
|
|
18
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
19
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(value))) return false;
|
|
20
|
+
const keys = Reflect.ownKeys(value);
|
|
21
|
+
const expectedKeys = Object.keys(expected);
|
|
22
|
+
if (keys.length !== expectedKeys.length || keys.some(key => typeof key !== 'string' || !expectedKeys.includes(key))) return false;
|
|
23
|
+
return expectedKeys.every(key => {
|
|
24
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
25
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) return false;
|
|
26
|
+
const expectedValue = expected[key];
|
|
27
|
+
return expectedValue && typeof expectedValue === 'object'
|
|
28
|
+
? exactObject(descriptor.value, expectedValue)
|
|
29
|
+
: descriptor.value === expectedValue;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function clientProfileFor(client) {
|
|
34
|
+
return client === 'claude' ? CLAUDE_FEATURE_PROFILE : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function matchesClientProfile(client, value) {
|
|
38
|
+
const expected = clientProfileFor(client);
|
|
39
|
+
return expected === undefined ? value === undefined : exactObject(value, expected);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function workerExecutionFor(role) {
|
|
43
|
+
if (role.harness === undefined) return undefined;
|
|
44
|
+
if (role.kind !== 'worker' || !['claude', 'codex'].includes(role.harness)) throw new TypeError('Invalid worker harness.');
|
|
45
|
+
const clientProfile = clientProfileFor(role.harness);
|
|
46
|
+
return Object.freeze({ client: role.harness, ...(clientProfile === undefined ? {} : { clientProfile }) });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function matchesWorkerExecution(role, execution) {
|
|
50
|
+
const expected = workerExecutionFor(role);
|
|
51
|
+
return expected === undefined ? execution === undefined : exactObject(execution, expected);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function executionForNode(plan, node) {
|
|
55
|
+
return node.execution ?? { client: plan.client, clientProfile: plan.clientProfile };
|
|
56
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import Ajv from 'ajv';
|
|
4
|
+
|
|
5
|
+
import { immutableJson } from '../clients/contract.js';
|
|
6
|
+
|
|
7
|
+
export const FEATURE_DECOMPOSITION_KIND = 'agilno.feature-decomposition';
|
|
8
|
+
|
|
9
|
+
const schema = JSON.parse(readFileSync(
|
|
10
|
+
new URL('../../schemas/feature-decomposition.schema.json', import.meta.url),
|
|
11
|
+
'utf8',
|
|
12
|
+
));
|
|
13
|
+
const validateSchema = new Ajv({ allErrors: true, strict: true }).compile(schema);
|
|
14
|
+
|
|
15
|
+
export class FeatureDecompositionError extends Error {
|
|
16
|
+
constructor() {
|
|
17
|
+
super('Feature decomposition is invalid.');
|
|
18
|
+
this.name = 'FeatureDecompositionError';
|
|
19
|
+
this.code = 'ERR_INVALID_FEATURE_DECOMPOSITION';
|
|
20
|
+
this.safeMessage = this.message;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fail() { throw new FeatureDecompositionError(); }
|
|
25
|
+
|
|
26
|
+
export function featureDecompositionResultContract() {
|
|
27
|
+
return immutableJson({
|
|
28
|
+
version: 1,
|
|
29
|
+
kind: FEATURE_DECOMPOSITION_KIND,
|
|
30
|
+
framing: 'Emit exactly one JSON object matching this result schema on stdout. Emit no Markdown or commentary.',
|
|
31
|
+
instructions: [
|
|
32
|
+
'Inspect the repository read-only and propose between one and sixteen implementation work items.',
|
|
33
|
+
'Use only normalized repository-relative owned paths; never use .git, .rivet, or configured sensitive paths.',
|
|
34
|
+
'Owned path components must not end in a dot or space or use Windows reserved names. Paths within each work item must be unique ignoring case.',
|
|
35
|
+
'If contract.repair is present, a prior proposal was rejected for its reason code. Reinspect the repository and return a complete corrected decomposition under the same policy; do not omit required implementation work to avoid validation.',
|
|
36
|
+
'List only files the work item is expected to change, including adjacent style modules, configuration, fixtures, and tests when they will be modified; do not include files that are merely read or reused, and never rely on an unlisted support file.',
|
|
37
|
+
'Allocate every request acceptance criterion at least once using one-based acceptance-criterion indexes; criteria may support multiple work items.',
|
|
38
|
+
'Do not emit roles, providers, authority, budgets, commands, evidence, dependencies, approval gates, or other policy fields.',
|
|
39
|
+
],
|
|
40
|
+
schema,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createFeatureDecomposition(value, acceptanceCriterionCount) {
|
|
45
|
+
let result;
|
|
46
|
+
try { result = immutableJson(value); } catch { fail(); }
|
|
47
|
+
if (!Number.isSafeInteger(acceptanceCriterionCount) || acceptanceCriterionCount < 1 || acceptanceCriterionCount > 256
|
|
48
|
+
|| !validateSchema(result)) fail();
|
|
49
|
+
const allocated = new Set();
|
|
50
|
+
for (const item of result.workItems) {
|
|
51
|
+
for (const index of item.acceptanceCriterionIndexes) {
|
|
52
|
+
if (index > acceptanceCriterionCount) fail();
|
|
53
|
+
allocated.add(index);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (allocated.size !== acceptanceCriterionCount) fail();
|
|
57
|
+
return result;
|
|
58
|
+
}
|