@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,253 @@
|
|
|
1
|
+
import { AgentContractError, immutableJson } from '../clients/contract.js';
|
|
2
|
+
import { validateProjectConfiguration } from '../config/validate.js';
|
|
3
|
+
import { validateWorkRequest } from '../work-request/contract.js';
|
|
4
|
+
import {
|
|
5
|
+
FeatureDecompositionError,
|
|
6
|
+
createFeatureDecomposition,
|
|
7
|
+
} from './decomposition-contract.js';
|
|
8
|
+
import {
|
|
9
|
+
FeaturePlanError,
|
|
10
|
+
createFeaturePlan,
|
|
11
|
+
featurePlanDigest,
|
|
12
|
+
} from './plan-contract.js';
|
|
13
|
+
import { clientProfileFor, workerExecutionFor } from './client-profile.js';
|
|
14
|
+
|
|
15
|
+
const MAX_PLANNING_ATTEMPTS = 2;
|
|
16
|
+
const REPAIRABLE_PATH_VIOLATIONS = new Set(['owned-path-invalid', 'owned-path-protected', 'owned-path-duplicate']);
|
|
17
|
+
|
|
18
|
+
function fail(reason) { throw new FeaturePlanError(reason); }
|
|
19
|
+
|
|
20
|
+
function slug(value, fallback = 'feature') {
|
|
21
|
+
const normalized = value.normalize('NFKD').replace(/\p{M}/gu, '').toLowerCase()
|
|
22
|
+
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
23
|
+
const candidate = /^[a-z]/.test(normalized) ? normalized : `${fallback}-${normalized}`;
|
|
24
|
+
return (candidate || fallback).slice(0, 64).replace(/-+$/g, '') || fallback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function uniqueNodeId(objective, used) {
|
|
28
|
+
const base = slug(objective, 'work-item');
|
|
29
|
+
let candidate = base;
|
|
30
|
+
let suffix = 2;
|
|
31
|
+
while (used.has(candidate)) {
|
|
32
|
+
const tail = `-${suffix}`;
|
|
33
|
+
candidate = `${base.slice(0, 64 - tail.length).replace(/-+$/g, '')}${tail}`;
|
|
34
|
+
suffix += 1;
|
|
35
|
+
}
|
|
36
|
+
used.add(candidate);
|
|
37
|
+
return candidate;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function intersect(...values) {
|
|
41
|
+
const [first = [], ...rest] = values;
|
|
42
|
+
return first.filter(value => rest.every(items => items.includes(value)));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function boundedBudget(...roles) {
|
|
46
|
+
return Object.freeze(Object.fromEntries(
|
|
47
|
+
['timeMinutes', 'tokenLimit', 'costUsd', 'taskLimit']
|
|
48
|
+
.map(name => [name, Math.min(...roles.map(role => role.budget[name]))]),
|
|
49
|
+
));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function roleChain(config) {
|
|
53
|
+
for (const boss of config.orchestration.roles.filter(role => role.kind === 'boss' && role.delegatesTo.includes('manager'))) {
|
|
54
|
+
for (const manager of config.orchestration.roles.filter(role => role.kind === 'manager'
|
|
55
|
+
&& role.delegatesTo.includes('worker'))) {
|
|
56
|
+
const worker = config.orchestration.roles.find(role => role.kind === 'worker');
|
|
57
|
+
if (worker) return { boss, manager, worker };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
fail('compiled-plan-invalid');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function requiredEvidence(config, role, { humanApproval = false } = {}) {
|
|
64
|
+
const profile = config.orchestration.completionProfiles.find(value => value.id === role.completionProfile);
|
|
65
|
+
if (!profile) fail('compiled-plan-invalid');
|
|
66
|
+
return [...new Set([...profile.requiredEvidence, ...(humanApproval ? ['human-approval'] : [])])];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function compileFeaturePlan({ decomposition, config, workRequest, baselineCommit, client }) {
|
|
70
|
+
if (client === 'host' && config.orchestration.roles.some(role => role.harness !== undefined)) fail('compiled-plan-invalid');
|
|
71
|
+
const { boss, manager, worker } = roleChain(config);
|
|
72
|
+
const execution = workerExecutionFor(worker);
|
|
73
|
+
const managerScopes = intersect(manager.authorityScopes, boss.authorityScopes);
|
|
74
|
+
const workerScopes = intersect(worker.authorityScopes, managerScopes);
|
|
75
|
+
if (managerScopes.length === 0 || workerScopes.length === 0) fail('compiled-plan-invalid');
|
|
76
|
+
const configuredProviders = new Set(config.providers.providers.map(provider => provider.id));
|
|
77
|
+
const providerRefs = intersect(boss.providerRefs, manager.providerRefs, worker.providerRefs)
|
|
78
|
+
.filter(provider => configuredProviders.has(provider));
|
|
79
|
+
const commandIds = config.quality.commandGates.filter(gate => gate.required).map(gate => gate.command);
|
|
80
|
+
const used = new Set(['activation', 'management', 'final-delivery']);
|
|
81
|
+
const workers = decomposition.workItems.map(item => ({
|
|
82
|
+
id: uniqueNodeId(item.objective, used),
|
|
83
|
+
parentId: 'management',
|
|
84
|
+
role: 'worker',
|
|
85
|
+
roleId: worker.id,
|
|
86
|
+
...(execution === undefined ? {} : { execution }),
|
|
87
|
+
objective: item.objective,
|
|
88
|
+
dependencies: ['management'],
|
|
89
|
+
ownedPaths: item.ownedPaths,
|
|
90
|
+
authorityScopes: workerScopes,
|
|
91
|
+
commandIds,
|
|
92
|
+
budget: boundedBudget(boss, manager, worker),
|
|
93
|
+
requiredEvidenceTypes: requiredEvidence(config, worker),
|
|
94
|
+
acceptanceCriteria: item.acceptanceCriterionIndexes.map(criterionIndex => (
|
|
95
|
+
workRequest.acceptanceCriteria[criterionIndex - 1]
|
|
96
|
+
)),
|
|
97
|
+
status: 'proposed',
|
|
98
|
+
}));
|
|
99
|
+
for (let index = 1; index < workers.length; index += 1) {
|
|
100
|
+
workers[index].dependencies = [workers[index - 1].id];
|
|
101
|
+
}
|
|
102
|
+
const finalScopes = boss.authorityScopes.includes('verify') ? ['verify'] : [boss.authorityScopes[0]];
|
|
103
|
+
const clientProfile = clientProfileFor(client);
|
|
104
|
+
return {
|
|
105
|
+
schemaVersion: 1,
|
|
106
|
+
id: slug(workRequest.title),
|
|
107
|
+
baselineCommit,
|
|
108
|
+
workRequestDigest: workRequest.digest,
|
|
109
|
+
client,
|
|
110
|
+
...(clientProfile === undefined ? {} : { clientProfile }),
|
|
111
|
+
providerRefs,
|
|
112
|
+
nodes: [
|
|
113
|
+
{
|
|
114
|
+
id: 'activation', role: 'boss', roleId: boss.id, objective: 'Approve the exact feature plan.',
|
|
115
|
+
dependencies: [], ownedPaths: [], authorityScopes: boss.authorityScopes, commandIds: [],
|
|
116
|
+
budget: boundedBudget(boss), requiredEvidenceTypes: requiredEvidence(config, boss, { humanApproval: true }),
|
|
117
|
+
acceptanceCriteria: [], status: 'proposed', approvalGate: 'activation',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'management', parentId: 'activation', role: 'manager', roleId: manager.id,
|
|
121
|
+
objective: 'Coordinate implementation and verification.', dependencies: ['activation'], ownedPaths: [],
|
|
122
|
+
authorityScopes: managerScopes, commandIds: [], budget: boundedBudget(boss, manager),
|
|
123
|
+
requiredEvidenceTypes: requiredEvidence(config, manager), acceptanceCriteria: [], status: 'proposed',
|
|
124
|
+
},
|
|
125
|
+
...workers,
|
|
126
|
+
{
|
|
127
|
+
id: 'final-delivery', parentId: 'activation', role: 'boss', roleId: boss.id,
|
|
128
|
+
objective: 'Review evidence and approve final delivery.', dependencies: workers.map(node => node.id),
|
|
129
|
+
ownedPaths: [], authorityScopes: finalScopes, commandIds,
|
|
130
|
+
budget: boundedBudget(boss), requiredEvidenceTypes: requiredEvidence(config, boss, { humanApproval: true }),
|
|
131
|
+
acceptanceCriteria: [], status: 'proposed', approvalGate: 'final-delivery',
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function createHostFeaturePlan(input) {
|
|
138
|
+
try {
|
|
139
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)
|
|
140
|
+
|| Reflect.ownKeys(input).length !== 4
|
|
141
|
+
|| !['config', 'workRequest', 'baselineCommit', 'decomposition'].every(key => Object.hasOwn(input, key))) fail();
|
|
142
|
+
validateProjectConfiguration(input.config);
|
|
143
|
+
validateWorkRequest(input.workRequest);
|
|
144
|
+
const decomposition = createFeatureDecomposition(
|
|
145
|
+
input.decomposition,
|
|
146
|
+
input.workRequest.acceptanceCriteria.length,
|
|
147
|
+
);
|
|
148
|
+
const bindings = {
|
|
149
|
+
config: input.config,
|
|
150
|
+
workRequest: input.workRequest,
|
|
151
|
+
baselineCommit: input.baselineCommit,
|
|
152
|
+
client: 'host',
|
|
153
|
+
};
|
|
154
|
+
return createFeaturePlan({
|
|
155
|
+
proposal: compileFeaturePlan({ decomposition, ...bindings }),
|
|
156
|
+
...bindings,
|
|
157
|
+
});
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error instanceof FeaturePlanError) throw error;
|
|
160
|
+
if (error instanceof FeatureDecompositionError) fail('decomposition-invalid');
|
|
161
|
+
fail();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function planningContract({ config, workRequest, baselineCommit, client }) {
|
|
166
|
+
return immutableJson({
|
|
167
|
+
schemaVersion: 1,
|
|
168
|
+
baselineCommit,
|
|
169
|
+
client,
|
|
170
|
+
workRequest,
|
|
171
|
+
policy: {
|
|
172
|
+
projectId: config.project.id,
|
|
173
|
+
defaultBranch: config.project.repository.defaultBranch,
|
|
174
|
+
branchPattern: config.project.repository.branchPattern,
|
|
175
|
+
sensitivePaths: config.project.repository.sensitivePaths ?? [],
|
|
176
|
+
commands: config.project.commands,
|
|
177
|
+
providers: config.providers.providers.map(provider => ({
|
|
178
|
+
id: provider.id, kind: provider.kind, mode: provider.mode, capabilities: provider.capabilities,
|
|
179
|
+
})),
|
|
180
|
+
roles: config.orchestration.roles.map(role => ({
|
|
181
|
+
id: role.id, kind: role.kind, delegatesTo: role.delegatesTo, providerRefs: role.providerRefs,
|
|
182
|
+
authorityScopes: role.authorityScopes, permissions: role.permissions, budget: role.budget,
|
|
183
|
+
completionProfile: role.completionProfile,
|
|
184
|
+
})),
|
|
185
|
+
approvalGates: config.orchestration.approvalGates,
|
|
186
|
+
completionProfiles: config.orchestration.completionProfiles,
|
|
187
|
+
quality: {
|
|
188
|
+
commandGates: config.quality.commandGates,
|
|
189
|
+
evidence: config.quality.evidence,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function parseProposal(value) {
|
|
196
|
+
if (typeof value !== 'string') return value;
|
|
197
|
+
if (Buffer.byteLength(value) > 512 * 1024) fail();
|
|
198
|
+
try { return JSON.parse(value); } catch { fail(); }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function createFeaturePlanner(input) {
|
|
202
|
+
try {
|
|
203
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)
|
|
204
|
+
|| Reflect.ownKeys(input).length !== 1 || !Object.hasOwn(input, 'planningClient')
|
|
205
|
+
|| !input.planningClient || typeof input.planningClient !== 'object'
|
|
206
|
+
|| typeof input.planningClient.propose !== 'function') fail();
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (error instanceof FeaturePlanError) throw error;
|
|
209
|
+
fail();
|
|
210
|
+
}
|
|
211
|
+
const planningClient = input.planningClient;
|
|
212
|
+
return Object.freeze({
|
|
213
|
+
async propose(request) {
|
|
214
|
+
try {
|
|
215
|
+
if (!request || typeof request !== 'object' || Array.isArray(request)
|
|
216
|
+
|| Reflect.ownKeys(request).length !== 4
|
|
217
|
+
|| !['config', 'workRequest', 'baselineCommit', 'client'].every(key => Object.hasOwn(request, key))) fail();
|
|
218
|
+
validateProjectConfiguration(request.config);
|
|
219
|
+
validateWorkRequest(request.workRequest);
|
|
220
|
+
const contract = planningContract(request);
|
|
221
|
+
let repair;
|
|
222
|
+
// At most two read-only provider calls; never retry provider or policy failures.
|
|
223
|
+
for (let attempt = 1; attempt <= MAX_PLANNING_ATTEMPTS; attempt += 1) {
|
|
224
|
+
const payload = repair ? immutableJson({ ...contract, repair }) : contract;
|
|
225
|
+
const proposal = parseProposal(await planningClient.propose(payload));
|
|
226
|
+
let decomposition;
|
|
227
|
+
try {
|
|
228
|
+
decomposition = createFeatureDecomposition(proposal, request.workRequest.acceptanceCriteria.length);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (error instanceof FeatureDecompositionError) fail('decomposition-invalid');
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
const compiled = compileFeaturePlan({ decomposition, ...request });
|
|
234
|
+
try { return createFeaturePlan({ proposal: compiled, ...request }); }
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (!(error instanceof FeaturePlanError)) throw error;
|
|
237
|
+
const violation = error.details.violation;
|
|
238
|
+
if (attempt < MAX_PLANNING_ATTEMPTS && REPAIRABLE_PATH_VIOLATIONS.has(violation)) {
|
|
239
|
+
repair = { attempt: attempt + 1, reason: violation };
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
throw new FeaturePlanError('compiled-plan-invalid', violation);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (error instanceof FeaturePlanError || error instanceof AgentContractError) throw error;
|
|
247
|
+
fail();
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export { FeaturePlanError, featurePlanDigest };
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { immutableJson } from '../clients/contract.js';
|
|
4
|
+
import { createSnapshotStore, readSnapshotWithoutLock } from '../state/snapshot-store.js';
|
|
5
|
+
import { assertResolvedStatePaths } from '../state/paths.js';
|
|
6
|
+
import { validateWorkRequest } from '../work-request/contract.js';
|
|
7
|
+
import { featurePlanDigest, validateFeaturePlan } from './plan-contract.js';
|
|
8
|
+
|
|
9
|
+
const STATUSES = new Set(['proposed', 'approved', 'running', 'blocked', 'awaiting-final-approval', 'completed', 'cancelled']);
|
|
10
|
+
const TRANSITIONS = Object.freeze({
|
|
11
|
+
proposed: new Set(['approved', 'cancelled']),
|
|
12
|
+
approved: new Set(['running', 'cancelled']),
|
|
13
|
+
running: new Set(['blocked', 'awaiting-final-approval', 'completed', 'cancelled']),
|
|
14
|
+
blocked: new Set(['running', 'cancelled']),
|
|
15
|
+
'awaiting-final-approval': new Set(['completed', 'cancelled']),
|
|
16
|
+
completed: new Set(),
|
|
17
|
+
cancelled: new Set(),
|
|
18
|
+
});
|
|
19
|
+
const DIGEST = /^[a-f0-9]{64}$/;
|
|
20
|
+
const REF = /^[a-z][a-z0-9-]{0,63}:[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
21
|
+
const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
22
|
+
const RECORD_KEYS = new Set([
|
|
23
|
+
'schemaVersion', 'runId', 'status', 'workRequest', 'featurePlan', 'proposalDigest',
|
|
24
|
+
'tracker', 'activation', 'runtimeRefs', 'evidenceRefs', 'createdAt', 'updatedAt',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
export class FeatureRunStoreError extends Error {
|
|
28
|
+
constructor() {
|
|
29
|
+
super('Feature run state is invalid.');
|
|
30
|
+
this.name = 'FeatureRunStoreError';
|
|
31
|
+
this.code = 'ERR_INVALID_FEATURE_RUN';
|
|
32
|
+
this.safeMessage = this.message;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class FeatureRunVersionConflictError extends Error {
|
|
37
|
+
constructor() {
|
|
38
|
+
super('Feature run version conflict.');
|
|
39
|
+
this.name = 'FeatureRunVersionConflictError';
|
|
40
|
+
this.code = 'ERR_FEATURE_RUN_VERSION_CONFLICT';
|
|
41
|
+
this.safeMessage = this.message;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fail() { throw new FeatureRunStoreError(); }
|
|
46
|
+
|
|
47
|
+
function canonical(value) {
|
|
48
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
49
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
50
|
+
return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function contentDigest(value) {
|
|
54
|
+
return createHash('sha256').update(canonical(value)).digest('hex');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function timestamp(value) {
|
|
58
|
+
if (typeof value !== 'string' || value.length > 64) fail();
|
|
59
|
+
let normalized;
|
|
60
|
+
try { normalized = new Date(value).toISOString(); } catch { fail(); }
|
|
61
|
+
if (normalized !== value) fail();
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function exactRecord(value, keys, required = keys) {
|
|
66
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) fail();
|
|
67
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
68
|
+
if (ownKeys.length > keys.size || ownKeys.some(key => typeof key !== 'string' || !keys.has(key))
|
|
69
|
+
|| [...required].some(key => !ownKeys.includes(key))) fail();
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function refs(value) {
|
|
74
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype || value.length > 256
|
|
75
|
+
|| value.some(ref => typeof ref !== 'string' || !REF.test(ref)) || new Set(value).size !== value.length) fail();
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function tracker(value, request) {
|
|
80
|
+
if (value === null) {
|
|
81
|
+
if (request.source.kind === 'jira' || request.source.kind === 'linear') fail();
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
exactRecord(value, new Set(['provider', 'ticketId', 'capturedRevision', 'currentRevision', 'drifted', 'checkedAt']));
|
|
85
|
+
if (value.provider !== request.source.kind || value.ticketId !== request.source.ref
|
|
86
|
+
|| value.capturedRevision !== request.source.revision || typeof value.currentRevision !== 'string'
|
|
87
|
+
|| value.currentRevision.length < 1 || value.currentRevision.length > 256
|
|
88
|
+
|| typeof value.drifted !== 'boolean' || value.drifted !== (value.currentRevision !== value.capturedRevision)) fail();
|
|
89
|
+
timestamp(value.checkedAt);
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function activation(value, record) {
|
|
94
|
+
if (value === null) return null;
|
|
95
|
+
exactRecord(value, new Set(['approverId', 'approvedAt', 'requestDigest', 'proposalDigest']));
|
|
96
|
+
if (typeof value.approverId !== 'string' || !ID.test(value.approverId)
|
|
97
|
+
|| value.requestDigest !== record.workRequest.digest || value.proposalDigest !== record.proposalDigest) fail();
|
|
98
|
+
timestamp(value.approvedAt);
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function validateRecord(value, runId) {
|
|
103
|
+
exactRecord(value, RECORD_KEYS);
|
|
104
|
+
if (value.schemaVersion !== 1 || value.runId !== runId || !STATUSES.has(value.status)) fail();
|
|
105
|
+
try { validateWorkRequest(value.workRequest); } catch { fail(); }
|
|
106
|
+
if (!value.featurePlan || typeof value.featurePlan !== 'object' || Array.isArray(value.featurePlan)
|
|
107
|
+
|| typeof value.proposalDigest !== 'string' || !DIGEST.test(value.proposalDigest)
|
|
108
|
+
|| contentDigest(value.featurePlan) !== value.proposalDigest
|
|
109
|
+
|| value.featurePlan.workRequestDigest !== value.workRequest.digest) fail();
|
|
110
|
+
tracker(value.tracker, value.workRequest);
|
|
111
|
+
activation(value.activation, value);
|
|
112
|
+
refs(value.runtimeRefs);
|
|
113
|
+
refs(value.evidenceRefs);
|
|
114
|
+
const created = timestamp(value.createdAt);
|
|
115
|
+
const updated = timestamp(value.updatedAt);
|
|
116
|
+
if (Date.parse(updated) < Date.parse(created) || value.status !== 'proposed' && value.activation === null) fail();
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function hydrate(snapshot, runId) {
|
|
121
|
+
if (!snapshot || !Number.isSafeInteger(snapshot.version) || snapshot.version < 1) fail();
|
|
122
|
+
const record = immutableJson({ ...snapshot.data, version: snapshot.version });
|
|
123
|
+
const data = { ...record };
|
|
124
|
+
delete data.version;
|
|
125
|
+
validateRecord(data, runId);
|
|
126
|
+
return record;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function createTracker(request, at) {
|
|
130
|
+
if (request.source.kind !== 'jira' && request.source.kind !== 'linear') return null;
|
|
131
|
+
if (!request.source.revision) fail();
|
|
132
|
+
return {
|
|
133
|
+
provider: request.source.kind,
|
|
134
|
+
ticketId: request.source.ref,
|
|
135
|
+
capturedRevision: request.source.revision,
|
|
136
|
+
currentRevision: request.source.revision,
|
|
137
|
+
drifted: false,
|
|
138
|
+
checkedAt: at,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function mapVersionConflict(error) {
|
|
143
|
+
if (error?.code === 'ERR_STATE_VERSION_CONFLICT' || /state version conflict/i.test(error?.message ?? '')) {
|
|
144
|
+
throw new FeatureRunVersionConflictError();
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function createFeatureRunStore(paths) {
|
|
150
|
+
assertResolvedStatePaths(paths);
|
|
151
|
+
if (typeof paths.runId !== 'string' || !ID.test(paths.runId) || paths.runDir !== paths.instanceDir) fail();
|
|
152
|
+
const snapshots = createSnapshotStore(paths, { environment: {} });
|
|
153
|
+
|
|
154
|
+
async function read() {
|
|
155
|
+
const snapshot = await snapshots.read();
|
|
156
|
+
return snapshot === null ? null : hydrate(snapshot, paths.runId);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return Object.freeze({
|
|
160
|
+
read,
|
|
161
|
+
async readOnly() {
|
|
162
|
+
const snapshot = await readSnapshotWithoutLock(paths);
|
|
163
|
+
return snapshot === null ? null : hydrate(snapshot, paths.runId);
|
|
164
|
+
},
|
|
165
|
+
async create(input) {
|
|
166
|
+
try {
|
|
167
|
+
exactRecord(input, new Set(['workRequest', 'featurePlan', 'createdAt']));
|
|
168
|
+
validateWorkRequest(input.workRequest);
|
|
169
|
+
validateFeaturePlan(input.featurePlan);
|
|
170
|
+
if (input.featurePlan.workRequestDigest !== input.workRequest.digest) fail();
|
|
171
|
+
const createdAt = timestamp(input.createdAt);
|
|
172
|
+
const record = {
|
|
173
|
+
schemaVersion: 1,
|
|
174
|
+
runId: paths.runId,
|
|
175
|
+
status: 'proposed',
|
|
176
|
+
workRequest: input.workRequest,
|
|
177
|
+
featurePlan: input.featurePlan,
|
|
178
|
+
proposalDigest: featurePlanDigest(input.featurePlan),
|
|
179
|
+
tracker: createTracker(input.workRequest, createdAt),
|
|
180
|
+
activation: null,
|
|
181
|
+
runtimeRefs: [],
|
|
182
|
+
evidenceRefs: [],
|
|
183
|
+
createdAt,
|
|
184
|
+
updatedAt: createdAt,
|
|
185
|
+
};
|
|
186
|
+
validateRecord(record, paths.runId);
|
|
187
|
+
return hydrate(await snapshots.write(record, { expectedVersion: 0 }), paths.runId);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error instanceof FeatureRunStoreError || error instanceof FeatureRunVersionConflictError) throw error;
|
|
190
|
+
try { mapVersionConflict(error); } catch (mapped) {
|
|
191
|
+
if (mapped instanceof FeatureRunVersionConflictError) throw mapped;
|
|
192
|
+
}
|
|
193
|
+
fail();
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
async update(input, options = {}) {
|
|
197
|
+
try {
|
|
198
|
+
const allowed = new Set(['status', 'updatedAt', 'activation', 'trackerRevision', 'runtimeRefs', 'evidenceRefs']);
|
|
199
|
+
exactRecord(input, allowed, new Set(['status', 'updatedAt', 'runtimeRefs', 'evidenceRefs']));
|
|
200
|
+
if (!Number.isSafeInteger(options.expectedVersion) || options.expectedVersion < 1) fail();
|
|
201
|
+
const current = await read();
|
|
202
|
+
if (!current || current.version !== options.expectedVersion) throw new FeatureRunVersionConflictError();
|
|
203
|
+
if (!STATUSES.has(input.status) || !TRANSITIONS[current.status].has(input.status)) fail();
|
|
204
|
+
const updatedAt = timestamp(input.updatedAt);
|
|
205
|
+
if (Date.parse(updatedAt) < Date.parse(current.updatedAt)) fail();
|
|
206
|
+
refs(input.runtimeRefs);
|
|
207
|
+
refs(input.evidenceRefs);
|
|
208
|
+
if (current.runtimeRefs.some(ref => !input.runtimeRefs.includes(ref))
|
|
209
|
+
|| current.evidenceRefs.some(ref => !input.evidenceRefs.includes(ref))) fail();
|
|
210
|
+
const next = {
|
|
211
|
+
...current,
|
|
212
|
+
status: input.status,
|
|
213
|
+
updatedAt,
|
|
214
|
+
runtimeRefs: input.runtimeRefs,
|
|
215
|
+
evidenceRefs: input.evidenceRefs,
|
|
216
|
+
activation: input.activation ?? current.activation,
|
|
217
|
+
};
|
|
218
|
+
delete next.version;
|
|
219
|
+
if (current.activation && input.activation && canonical(current.activation) !== canonical(input.activation)) fail();
|
|
220
|
+
if (input.trackerRevision !== undefined) {
|
|
221
|
+
if (!current.tracker || typeof input.trackerRevision !== 'string' || input.trackerRevision.length < 1
|
|
222
|
+
|| input.trackerRevision.length > 256) fail();
|
|
223
|
+
next.tracker = {
|
|
224
|
+
...current.tracker,
|
|
225
|
+
currentRevision: input.trackerRevision,
|
|
226
|
+
drifted: input.trackerRevision !== current.tracker.capturedRevision,
|
|
227
|
+
checkedAt: updatedAt,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
validateRecord(next, paths.runId);
|
|
231
|
+
return hydrate(await snapshots.write(next, { expectedVersion: options.expectedVersion }), paths.runId);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error instanceof FeatureRunStoreError || error instanceof FeatureRunVersionConflictError) throw error;
|
|
234
|
+
try { mapVersionConflict(error); } catch (mapped) {
|
|
235
|
+
if (mapped instanceof FeatureRunVersionConflictError) throw mapped;
|
|
236
|
+
}
|
|
237
|
+
fail();
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|