@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,310 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { lstat, open, rename, unlink } from 'node:fs/promises';
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
|
+
|
|
7
|
+
import { acquireLock, LockBusyError } from './lock.js';
|
|
8
|
+
import { assertResolvedStatePaths, verifyResolvedStatePaths } from './paths.js';
|
|
9
|
+
import { redactSecrets } from './redact.js';
|
|
10
|
+
|
|
11
|
+
const MAX_SNAPSHOT_BYTES = 4 * 1024 * 1024;
|
|
12
|
+
const LOCK_WAIT_MS = 2_000;
|
|
13
|
+
const NOFOLLOW = constants.O_NOFOLLOW ?? 0;
|
|
14
|
+
const DIRECTORY = constants.O_DIRECTORY ?? 0;
|
|
15
|
+
|
|
16
|
+
function sameIdentity(left, right) {
|
|
17
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function privateRegularFile(metadata) {
|
|
21
|
+
return metadata.isFile()
|
|
22
|
+
&& !metadata.isSymbolicLink()
|
|
23
|
+
&& metadata.nlink === 1
|
|
24
|
+
&& (metadata.mode & 0o777) === 0o600;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class StateVersionExhaustedError extends Error {
|
|
28
|
+
constructor() {
|
|
29
|
+
super('state version is exhausted');
|
|
30
|
+
this.name = 'StateVersionExhaustedError';
|
|
31
|
+
this.code = 'ERR_STATE_VERSION_EXHAUSTED';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class SnapshotDurabilityError extends Error {
|
|
36
|
+
constructor() {
|
|
37
|
+
super('Private snapshot durability is unsupported on this filesystem');
|
|
38
|
+
this.name = 'SnapshotDurabilityError';
|
|
39
|
+
this.code = 'ERR_STATE_DURABILITY_UNSUPPORTED';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function metadataOrNull(path) {
|
|
44
|
+
try {
|
|
45
|
+
const metadata = await lstat(path);
|
|
46
|
+
if (!privateRegularFile(metadata)) throw new Error('Unsafe state snapshot');
|
|
47
|
+
return metadata;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error.code === 'ENOENT') return null;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validSnapshot(snapshot) {
|
|
55
|
+
return snapshot
|
|
56
|
+
&& typeof snapshot === 'object'
|
|
57
|
+
&& !Array.isArray(snapshot)
|
|
58
|
+
&& Object.keys(snapshot).length === 2
|
|
59
|
+
&& Number.isSafeInteger(snapshot.version)
|
|
60
|
+
&& snapshot.version >= 1
|
|
61
|
+
&& Object.hasOwn(snapshot, 'data');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readBoundedUtf8(handle, maximumBytes) {
|
|
65
|
+
const buffer = Buffer.alloc(maximumBytes + 1);
|
|
66
|
+
let offset = 0;
|
|
67
|
+
while (offset < buffer.length) {
|
|
68
|
+
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, null);
|
|
69
|
+
if (bytesRead === 0) break;
|
|
70
|
+
offset += bytesRead;
|
|
71
|
+
}
|
|
72
|
+
if (offset > maximumBytes) throw new Error('Invalid state snapshot');
|
|
73
|
+
try {
|
|
74
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(buffer.subarray(0, offset));
|
|
75
|
+
} catch {
|
|
76
|
+
throw new Error('Invalid state snapshot');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function readSnapshot(path) {
|
|
81
|
+
const before = await metadataOrNull(path);
|
|
82
|
+
if (!before) return null;
|
|
83
|
+
if (before.size <= 0 || before.size > MAX_SNAPSHOT_BYTES) throw new Error('Invalid state snapshot');
|
|
84
|
+
let handle;
|
|
85
|
+
try {
|
|
86
|
+
handle = await open(path, constants.O_RDONLY | NOFOLLOW);
|
|
87
|
+
const opened = await handle.stat();
|
|
88
|
+
if (!privateRegularFile(opened) || !sameIdentity(before, opened)) throw new Error('Unsafe state snapshot');
|
|
89
|
+
const source = await readBoundedUtf8(handle, MAX_SNAPSHOT_BYTES);
|
|
90
|
+
const after = await lstat(path);
|
|
91
|
+
if (!privateRegularFile(after) || !sameIdentity(opened, after) || after.size !== opened.size) {
|
|
92
|
+
throw new Error('State snapshot identity changed');
|
|
93
|
+
}
|
|
94
|
+
if (!source.endsWith('\n')) throw new Error('Invalid state snapshot');
|
|
95
|
+
let snapshot;
|
|
96
|
+
try { snapshot = JSON.parse(source); } catch { throw new Error('Invalid state snapshot'); }
|
|
97
|
+
if (!validSnapshot(snapshot)) throw new Error('Invalid state snapshot');
|
|
98
|
+
return snapshot;
|
|
99
|
+
} finally {
|
|
100
|
+
await handle?.close();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function acquireWithRetry(path, options) {
|
|
105
|
+
const lockWaitMs = options.lockWaitMs ?? LOCK_WAIT_MS;
|
|
106
|
+
if (!Number.isSafeInteger(lockWaitMs) || lockWaitMs < 0 || lockWaitMs > 60_000) {
|
|
107
|
+
throw new TypeError('Invalid state lock wait');
|
|
108
|
+
}
|
|
109
|
+
const deadline = Date.now() + lockWaitMs;
|
|
110
|
+
while (true) {
|
|
111
|
+
try {
|
|
112
|
+
return await acquireLock(path, options.lockOptions);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (!(error instanceof LockBusyError) || Date.now() >= deadline) throw error;
|
|
115
|
+
await delay(5);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function openPrivateDirectory(path) {
|
|
121
|
+
const before = await lstat(path);
|
|
122
|
+
if (!before.isDirectory() || before.isSymbolicLink() || (before.mode & 0o777) !== 0o700) {
|
|
123
|
+
throw new Error('Private state path identity changed');
|
|
124
|
+
}
|
|
125
|
+
const handle = await open(path, constants.O_RDONLY | DIRECTORY | NOFOLLOW);
|
|
126
|
+
try {
|
|
127
|
+
const opened = await handle.stat();
|
|
128
|
+
if (!opened.isDirectory() || !sameIdentity(before, opened)) throw new Error('Private state path identity changed');
|
|
129
|
+
return handle;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
await handle.close();
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function verifyPrivateDirectory(paths, handle) {
|
|
137
|
+
await verifyResolvedStatePaths(paths);
|
|
138
|
+
const [opened, current] = await Promise.all([handle.stat(), lstat(paths.instanceDir)]);
|
|
139
|
+
if (!opened.isDirectory() || !current.isDirectory() || !sameIdentity(opened, current)) {
|
|
140
|
+
throw new Error('Private state path identity changed');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function syncDirectory(handle) {
|
|
145
|
+
try {
|
|
146
|
+
await handle.sync();
|
|
147
|
+
} catch {
|
|
148
|
+
throw new SnapshotDurabilityError();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function cleanupTemporary(paths, directoryHandle, temporaryPath, expected) {
|
|
153
|
+
if (!expected) return;
|
|
154
|
+
try {
|
|
155
|
+
await verifyPrivateDirectory(paths, directoryHandle);
|
|
156
|
+
const current = await lstat(temporaryPath);
|
|
157
|
+
if (privateRegularFile(current) && sameIdentity(current, expected)) await unlink(temporaryPath);
|
|
158
|
+
} catch {}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function atomicWrite(paths, source, temporaryId, directoryHandle) {
|
|
162
|
+
const path = paths.snapshotPath;
|
|
163
|
+
const temporaryPath = join(paths.instanceDir, `.${basename(path)}.${temporaryId}.tmp`);
|
|
164
|
+
const targetBefore = await metadataOrNull(path);
|
|
165
|
+
let handle;
|
|
166
|
+
let createdMetadata;
|
|
167
|
+
let temporaryMetadata;
|
|
168
|
+
try {
|
|
169
|
+
handle = await open(
|
|
170
|
+
temporaryPath,
|
|
171
|
+
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | NOFOLLOW,
|
|
172
|
+
0o600,
|
|
173
|
+
);
|
|
174
|
+
createdMetadata = await handle.stat();
|
|
175
|
+
if (!privateRegularFile(createdMetadata) || createdMetadata.size !== 0) {
|
|
176
|
+
throw new Error('Unsafe state snapshot temporary file');
|
|
177
|
+
}
|
|
178
|
+
await handle.chmod(0o600);
|
|
179
|
+
await handle.writeFile(source, { encoding: 'utf8' });
|
|
180
|
+
await handle.sync();
|
|
181
|
+
temporaryMetadata = await handle.stat();
|
|
182
|
+
if (
|
|
183
|
+
!privateRegularFile(temporaryMetadata)
|
|
184
|
+
|| !sameIdentity(createdMetadata, temporaryMetadata)
|
|
185
|
+
|| temporaryMetadata.size !== Buffer.byteLength(source)
|
|
186
|
+
) {
|
|
187
|
+
throw new Error('State snapshot temporary identity changed');
|
|
188
|
+
}
|
|
189
|
+
await handle.close();
|
|
190
|
+
handle = null;
|
|
191
|
+
await verifyPrivateDirectory(paths, directoryHandle);
|
|
192
|
+
const targetNow = await metadataOrNull(path);
|
|
193
|
+
if (
|
|
194
|
+
(targetBefore === null) !== (targetNow === null)
|
|
195
|
+
|| (targetBefore && !sameIdentity(targetBefore, targetNow))
|
|
196
|
+
) {
|
|
197
|
+
throw new Error('State snapshot identity changed');
|
|
198
|
+
}
|
|
199
|
+
const temporaryNow = await lstat(temporaryPath);
|
|
200
|
+
if (!privateRegularFile(temporaryNow) || !sameIdentity(temporaryNow, temporaryMetadata)) {
|
|
201
|
+
throw new Error('State snapshot temporary identity changed');
|
|
202
|
+
}
|
|
203
|
+
await rename(temporaryPath, path);
|
|
204
|
+
await syncDirectory(directoryHandle);
|
|
205
|
+
const persisted = await metadataOrNull(path);
|
|
206
|
+
if (!persisted || !sameIdentity(persisted, temporaryMetadata)) throw new Error('State snapshot was not persisted');
|
|
207
|
+
} catch (error) {
|
|
208
|
+
await cleanupTemporary(paths, directoryHandle, temporaryPath, temporaryMetadata ?? createdMetadata);
|
|
209
|
+
if (error.code === 'EEXIST') throw new Error('Unsafe state snapshot temporary file');
|
|
210
|
+
throw error;
|
|
211
|
+
} finally {
|
|
212
|
+
await handle?.close();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function assertPaths(paths) {
|
|
217
|
+
assertResolvedStatePaths(paths);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function captureLockOptions(lockOptions) {
|
|
221
|
+
if (lockOptions === undefined) return undefined;
|
|
222
|
+
if (!lockOptions || typeof lockOptions !== 'object') throw new TypeError('Invalid state lock options');
|
|
223
|
+
return Object.freeze({
|
|
224
|
+
staleAfterMs: lockOptions.staleAfterMs,
|
|
225
|
+
now: lockOptions.now,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function captureStoreOptions(options) {
|
|
230
|
+
const beforeCommit = options.beforeCommit;
|
|
231
|
+
const temporaryIdFactory = options.temporaryIdFactory ?? randomUUID;
|
|
232
|
+
if (beforeCommit !== undefined && typeof beforeCommit !== 'function') {
|
|
233
|
+
throw new TypeError('Invalid snapshot hook');
|
|
234
|
+
}
|
|
235
|
+
if (typeof temporaryIdFactory !== 'function') throw new TypeError('Invalid snapshot temporary ID factory');
|
|
236
|
+
return Object.freeze({
|
|
237
|
+
lockWaitMs: options.lockWaitMs,
|
|
238
|
+
lockOptions: captureLockOptions(options.lockOptions),
|
|
239
|
+
secretKeys: options.secretKeys,
|
|
240
|
+
environment: options.environment,
|
|
241
|
+
beforeCommit,
|
|
242
|
+
temporaryIdFactory,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function createSnapshotStore(paths, options = {}) {
|
|
247
|
+
assertPaths(paths);
|
|
248
|
+
const storeOptions = captureStoreOptions(options);
|
|
249
|
+
|
|
250
|
+
async function withLock(operation) {
|
|
251
|
+
await verifyResolvedStatePaths(paths);
|
|
252
|
+
const lock = await acquireWithRetry(paths.lockPath, storeOptions);
|
|
253
|
+
try {
|
|
254
|
+
await verifyResolvedStatePaths(paths);
|
|
255
|
+
return await operation();
|
|
256
|
+
} finally {
|
|
257
|
+
await lock.release();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return Object.freeze({
|
|
262
|
+
async read() {
|
|
263
|
+
return withLock(() => readSnapshot(paths.snapshotPath));
|
|
264
|
+
},
|
|
265
|
+
async version() {
|
|
266
|
+
return withLock(async () => (await readSnapshot(paths.snapshotPath))?.version ?? 0);
|
|
267
|
+
},
|
|
268
|
+
async write(data, writeOptions = {}) {
|
|
269
|
+
const expectedVersion = writeOptions.expectedVersion;
|
|
270
|
+
const preparedData = redactSecrets(data, {
|
|
271
|
+
secretKeys: writeOptions.secretKeys ?? storeOptions.secretKeys,
|
|
272
|
+
environment: writeOptions.environment ?? storeOptions.environment ?? process.env,
|
|
273
|
+
});
|
|
274
|
+
if (!Number.isSafeInteger(expectedVersion) || expectedVersion < 0) {
|
|
275
|
+
throw new TypeError('Snapshot write requires a non-negative expectedVersion');
|
|
276
|
+
}
|
|
277
|
+
if (storeOptions.beforeCommit) await storeOptions.beforeCommit();
|
|
278
|
+
const temporaryId = storeOptions.temporaryIdFactory();
|
|
279
|
+
if (typeof temporaryId !== 'string' || !/^[A-Za-z0-9-]{1,64}$/.test(temporaryId)) {
|
|
280
|
+
throw new TypeError('Invalid snapshot temporary identifier');
|
|
281
|
+
}
|
|
282
|
+
return withLock(async () => {
|
|
283
|
+
const directoryHandle = await openPrivateDirectory(paths.instanceDir);
|
|
284
|
+
try {
|
|
285
|
+
await verifyPrivateDirectory(paths, directoryHandle);
|
|
286
|
+
const current = await readSnapshot(paths.snapshotPath);
|
|
287
|
+
const currentVersion = current?.version ?? 0;
|
|
288
|
+
if (expectedVersion !== currentVersion) throw new Error('state version conflict');
|
|
289
|
+
if (currentVersion >= Number.MAX_SAFE_INTEGER) throw new StateVersionExhaustedError();
|
|
290
|
+
const snapshot = {
|
|
291
|
+
version: currentVersion + 1,
|
|
292
|
+
data: preparedData,
|
|
293
|
+
};
|
|
294
|
+
const source = `${JSON.stringify(snapshot)}\n`;
|
|
295
|
+
if (Buffer.byteLength(source) > MAX_SNAPSHOT_BYTES) throw new Error('State snapshot exceeds storage limit');
|
|
296
|
+
await atomicWrite(paths, source, temporaryId, directoryHandle);
|
|
297
|
+
return snapshot;
|
|
298
|
+
} finally {
|
|
299
|
+
await directoryHandle.close();
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function readSnapshotWithoutLock(paths) {
|
|
307
|
+
assertPaths(paths);
|
|
308
|
+
await verifyResolvedStatePaths(paths);
|
|
309
|
+
return readSnapshot(paths.snapshotPath);
|
|
310
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const byId = id => document.getElementById(id);
|
|
2
|
+
|
|
3
|
+
function element(tag, text, className) {
|
|
4
|
+
const value = document.createElement(tag);
|
|
5
|
+
if (text !== undefined) value.textContent = String(text);
|
|
6
|
+
if (className) value.className = className;
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function duration(milliseconds) {
|
|
11
|
+
const minutes = Math.floor((milliseconds ?? 0) / 60_000);
|
|
12
|
+
const hours = Math.floor(minutes / 60);
|
|
13
|
+
return hours > 0 ? `${hours}h ${minutes % 60}m` : `${minutes}m`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function rows(target, values, render, empty = 'None') {
|
|
17
|
+
const children = values.length ? values.map(render) : [element('p', empty, 'muted')];
|
|
18
|
+
target.replaceChildren(...children);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function card(title, detail, status) {
|
|
22
|
+
const value = element('div', undefined, 'row');
|
|
23
|
+
const copy = element('div');
|
|
24
|
+
copy.append(element('strong', title), element('small', detail));
|
|
25
|
+
value.append(copy, element('span', status, `badge status-${status}`));
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function render(model) {
|
|
30
|
+
byId('goal-heading').textContent = model.goal.id;
|
|
31
|
+
byId('stage').textContent = model.currentStage
|
|
32
|
+
? `Current stage: ${model.currentStage.nodeId} · ${model.currentStage.status}`
|
|
33
|
+
: 'No active stage';
|
|
34
|
+
const metrics = [
|
|
35
|
+
['State', model.goal.status], ['Elapsed', duration(model.budget.elapsedMs)],
|
|
36
|
+
['Tokens', model.budget.usage.tokens], ['Cost', `$${model.budget.usage.costUsd}`],
|
|
37
|
+
['Heartbeat', model.latestHeartbeatAt ? new Date(model.latestHeartbeatAt).toLocaleTimeString() : '—'],
|
|
38
|
+
];
|
|
39
|
+
byId('metrics').replaceChildren(...metrics.map(([label, value]) => {
|
|
40
|
+
const metric = element('div', undefined, 'metric');
|
|
41
|
+
metric.append(element('span', label), element('strong', value));
|
|
42
|
+
return metric;
|
|
43
|
+
}));
|
|
44
|
+
rows(byId('graph'), model.graph.nodes, node => card(
|
|
45
|
+
node.id,
|
|
46
|
+
`${node.role} · parent ${node.parentId ?? 'root'} · depends on ${node.dependencies.join(', ') || 'none'}`,
|
|
47
|
+
node.status,
|
|
48
|
+
));
|
|
49
|
+
rows(byId('agents'), model.agents, agent => card(agent.id, `${agent.role} · ${agent.currentNodeId} · ${duration(agent.elapsedMs)}`, agent.status));
|
|
50
|
+
rows(byId('worktrees'), model.worktrees, worktree => card(
|
|
51
|
+
worktree.nodeId,
|
|
52
|
+
`${worktree.ownerId} · ${worktree.displayPath}`,
|
|
53
|
+
worktree.status,
|
|
54
|
+
), 'No owned worktrees');
|
|
55
|
+
rows(byId('gates'), model.gates, gate => card(gate.id, gate.requiredEvidence.join(' · '), gate.status), 'No human gates');
|
|
56
|
+
rows(byId('corrective'), model.correctiveWork, item => card(item.nodeId, `Attempt ${item.attempt}${item.reason ? ` · ${item.reason}` : ''}`, item.status), 'No corrective work');
|
|
57
|
+
rows(byId('evidence'), model.evidence, item => {
|
|
58
|
+
const value = card(item.id, `${item.type} · ${item.nodeId ?? 'goal'}`, item.approvalState);
|
|
59
|
+
if (item.url) {
|
|
60
|
+
const link = element('a', 'Open evidence', 'evidence-link');
|
|
61
|
+
link.href = item.url;
|
|
62
|
+
link.rel = 'noreferrer noopener';
|
|
63
|
+
value.append(link);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}, 'No evidence recorded');
|
|
67
|
+
rows(byId('decisions'), model.humanDecisions, item => card(item.gateId, `${item.actorId} · ${new Date(item.decidedAt).toLocaleTimeString()}`, item.decision), 'No human decisions');
|
|
68
|
+
rows(byId('events'), model.events.slice(-20).reverse(), item => card(item.type, `${item.nodeId ?? 'goal'} · #${item.sequence}`, item.actor.role), 'No events');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function refresh() {
|
|
72
|
+
const response = await fetch('/api/state', { cache: 'no-store' });
|
|
73
|
+
if (!response.ok) throw new Error('status unavailable');
|
|
74
|
+
render(await response.json());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function connect() {
|
|
78
|
+
const source = new EventSource('/events');
|
|
79
|
+
source.onopen = () => {
|
|
80
|
+
byId('connection').textContent = 'Live';
|
|
81
|
+
byId('connection-dot').className = 'connected';
|
|
82
|
+
};
|
|
83
|
+
source.addEventListener('status', () => { void refresh(); });
|
|
84
|
+
source.addEventListener('reset', () => { void refresh(); });
|
|
85
|
+
source.addEventListener('unavailable', () => {
|
|
86
|
+
byId('connection').textContent = 'Reconnecting';
|
|
87
|
+
byId('connection-dot').className = '';
|
|
88
|
+
});
|
|
89
|
+
source.onerror = () => {
|
|
90
|
+
byId('connection').textContent = 'Reconnecting';
|
|
91
|
+
byId('connection-dot').className = '';
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
refresh().catch(() => { byId('connection').textContent = 'Unavailable'; });
|
|
96
|
+
connect();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="color-scheme" content="dark">
|
|
7
|
+
<title>Orchestration status</title>
|
|
8
|
+
<link rel="stylesheet" href="/styles.css">
|
|
9
|
+
<script src="/app.js" defer></script>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<header class="masthead">
|
|
13
|
+
<div>
|
|
14
|
+
<p class="eyebrow">Local control room</p>
|
|
15
|
+
<h1>Orchestration status</h1>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="connection"><span id="connection-dot"></span><span id="connection">Connecting</span></div>
|
|
18
|
+
</header>
|
|
19
|
+
<main>
|
|
20
|
+
<section class="hero panel" aria-labelledby="goal-heading">
|
|
21
|
+
<div><p class="eyebrow">Active goal</p><h2 id="goal-heading">Loading…</h2><p id="stage"></p></div>
|
|
22
|
+
<div id="metrics" class="metrics"></div>
|
|
23
|
+
</section>
|
|
24
|
+
<section class="layout">
|
|
25
|
+
<article class="panel span-two"><h2>Goal graph</h2><div id="graph" class="graph"></div></article>
|
|
26
|
+
<article class="panel"><h2>Agents</h2><div id="agents" class="stack"></div></article>
|
|
27
|
+
<article class="panel"><h2>Owned worktrees</h2><div id="worktrees" class="stack"></div></article>
|
|
28
|
+
<article class="panel"><h2>Human gates</h2><div id="gates" class="stack"></div></article>
|
|
29
|
+
<article class="panel"><h2>Corrective work</h2><div id="corrective" class="stack"></div></article>
|
|
30
|
+
<article class="panel"><h2>Evidence</h2><div id="evidence" class="stack"></div></article>
|
|
31
|
+
<article class="panel"><h2>Decisions</h2><div id="decisions" class="stack"></div></article>
|
|
32
|
+
<article class="panel span-two"><h2>Recent events</h2><div id="events" class="timeline"></div></article>
|
|
33
|
+
</section>
|
|
34
|
+
</main>
|
|
35
|
+
<noscript>This local status view requires JavaScript.</noscript>
|
|
36
|
+
</body>
|
|
37
|
+
</html>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: dark;
|
|
3
|
+
--ink: #f6f8ff;
|
|
4
|
+
--muted: #9ba7bd;
|
|
5
|
+
--panel: rgba(17, 25, 42, 0.9);
|
|
6
|
+
--line: rgba(173, 192, 229, 0.15);
|
|
7
|
+
--accent: #79e2c5;
|
|
8
|
+
--blue: #7eb6ff;
|
|
9
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
* { box-sizing: border-box; }
|
|
13
|
+
body {
|
|
14
|
+
margin: 0;
|
|
15
|
+
min-height: 100vh;
|
|
16
|
+
color: var(--ink);
|
|
17
|
+
background: radial-gradient(circle at 12% 0%, #17365b 0, transparent 38rem), #070c16;
|
|
18
|
+
}
|
|
19
|
+
.masthead, main { width: min(1800px, calc(100% - 48px)); margin-inline: auto; }
|
|
20
|
+
.masthead { display: flex; align-items: center; justify-content: space-between; padding: 30px 0 18px; }
|
|
21
|
+
h1, h2, p { margin: 0; }
|
|
22
|
+
h1 { font-size: clamp(1.8rem, 3vw, 3.2rem); letter-spacing: -0.04em; }
|
|
23
|
+
h2 { margin-bottom: 18px; font-size: 1.05rem; }
|
|
24
|
+
.eyebrow { color: var(--accent); font-size: 0.72rem; font-weight: 800; letter-spacing: 0.16em; text-transform: uppercase; }
|
|
25
|
+
.connection { display: flex; align-items: center; gap: 10px; color: var(--muted); }
|
|
26
|
+
#connection-dot { width: 10px; height: 10px; border-radius: 50%; background: #f0b35c; box-shadow: 0 0 18px currentColor; }
|
|
27
|
+
#connection-dot.connected { background: var(--accent); }
|
|
28
|
+
.panel { padding: 22px; border: 1px solid var(--line); border-radius: 18px; background: var(--panel); backdrop-filter: blur(16px); }
|
|
29
|
+
.hero { display: flex; align-items: end; justify-content: space-between; gap: 28px; margin-bottom: 16px; }
|
|
30
|
+
.hero h2 { margin: 7px 0; font-size: clamp(1.5rem, 2.3vw, 2.6rem); }
|
|
31
|
+
#stage { color: var(--muted); }
|
|
32
|
+
.metrics { display: grid; grid-template-columns: repeat(5, minmax(100px, 1fr)); gap: 10px; }
|
|
33
|
+
.metric { min-width: 105px; padding: 12px; border-left: 2px solid var(--blue); background: rgba(255,255,255,0.03); }
|
|
34
|
+
.metric span, .row small { display: block; margin-bottom: 4px; color: var(--muted); font-size: 0.72rem; }
|
|
35
|
+
.layout { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; padding-bottom: 32px; }
|
|
36
|
+
.span-two { grid-column: span 2; }
|
|
37
|
+
.stack, .graph, .timeline { display: grid; gap: 9px; max-height: 420px; overflow: auto; }
|
|
38
|
+
.graph { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
39
|
+
.row { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 12px; padding: 11px 12px; border: 1px solid var(--line); border-radius: 11px; background: rgba(255,255,255,0.025); }
|
|
40
|
+
.row > div { min-width: 0; }
|
|
41
|
+
.row strong, .row small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
42
|
+
.badge { flex: none; padding: 4px 8px; border-radius: 999px; color: var(--blue); background: rgba(126,182,255,0.1); font-size: 0.68rem; text-transform: uppercase; }
|
|
43
|
+
.status-completed, .status-approved, .status-alive { color: var(--accent); background: rgba(121,226,197,0.1); }
|
|
44
|
+
.status-failed, .status-blocked, .status-corrective { color: #ff9ca5; background: rgba(255,91,110,0.1); }
|
|
45
|
+
.evidence-link { color: var(--accent); font-size: 0.75rem; }
|
|
46
|
+
.muted { color: var(--muted); }
|
|
47
|
+
|
|
48
|
+
/* Presentation resolution keeps every operational lane visible on a single 16:9 screen. */
|
|
49
|
+
@media (max-width: 1200px) { .layout { grid-template-columns: repeat(2, minmax(0, 1fr)); } .hero { align-items: stretch; flex-direction: column; } }
|
|
50
|
+
@media (max-width: 720px) { .masthead, main { width: min(100% - 24px, 1800px); } .layout, .graph, .metrics { grid-template-columns: 1fr; } .span-two { grid-column: span 1; } }
|