@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,171 @@
|
|
|
1
|
+
import { CliError, EXIT_CODES } from '../cli/output.js';
|
|
2
|
+
import { runQualityGates } from '../quality/runner.js';
|
|
3
|
+
import { validateTraceability } from '../quality/traceability.js';
|
|
4
|
+
|
|
5
|
+
function immutable(value) {
|
|
6
|
+
if (Array.isArray(value)) return Object.freeze(value.map(immutable));
|
|
7
|
+
if (value && typeof value === 'object') {
|
|
8
|
+
return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, child]) => [key, immutable(child)])));
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function snapshot(value, allowed, required) {
|
|
14
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('Verification input is invalid.');
|
|
15
|
+
let keys;
|
|
16
|
+
try { keys = Reflect.ownKeys(value); } catch { throw new TypeError('Verification input is invalid.'); }
|
|
17
|
+
if (keys.some(key => typeof key !== 'string' || !allowed.includes(key))) throw new TypeError('Verification input is invalid.');
|
|
18
|
+
const result = Object.create(null);
|
|
19
|
+
for (const key of keys) {
|
|
20
|
+
let descriptor;
|
|
21
|
+
try { descriptor = Object.getOwnPropertyDescriptor(value, key); } catch { throw new TypeError('Verification input is invalid.'); }
|
|
22
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) throw new TypeError('Verification input is invalid.');
|
|
23
|
+
result[key] = descriptor.value;
|
|
24
|
+
}
|
|
25
|
+
if (required.some(key => !Object.hasOwn(result, key))) throw new TypeError('Verification input is invalid.');
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function boundedArray(value, maximum, convert) {
|
|
30
|
+
try {
|
|
31
|
+
if (!Array.isArray(value)) throw new TypeError('Verification input is invalid.');
|
|
32
|
+
const length = Object.getOwnPropertyDescriptor(value, 'length')?.value;
|
|
33
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > maximum
|
|
34
|
+
|| Reflect.ownKeys(value).length !== length + 1) throw new TypeError('Verification input is invalid.');
|
|
35
|
+
const result = [];
|
|
36
|
+
for (let index = 0; index < length; index += 1) {
|
|
37
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
38
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) {
|
|
39
|
+
throw new TypeError('Verification input is invalid.');
|
|
40
|
+
}
|
|
41
|
+
result.push(convert(descriptor.value));
|
|
42
|
+
}
|
|
43
|
+
return Object.freeze(result);
|
|
44
|
+
} catch {
|
|
45
|
+
throw new TypeError('Verification input is invalid.');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function boundedString(value, maximum) {
|
|
50
|
+
if (typeof value !== 'string' || value.length < 1 || value.length > maximum || value.includes('\0')) {
|
|
51
|
+
throw new TypeError('Verification input is invalid.');
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function acceptanceCriterion(value) {
|
|
57
|
+
const criterion = snapshot(value, ['id', 'inScope'], ['id', 'inScope']);
|
|
58
|
+
if (typeof criterion.inScope !== 'boolean') throw new TypeError('Verification input is invalid.');
|
|
59
|
+
return Object.freeze({ id: boundedString(criterion.id, 100), inScope: criterion.inScope });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const APPROVAL_FIELDS = [
|
|
63
|
+
'id', 'approverId', 'approverPrincipal', 'subjectId', 'action', 'resource', 'policyId',
|
|
64
|
+
'decision', 'expiresAt', 'singleUse',
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
function immutableApproval(value) {
|
|
68
|
+
const approval = snapshot(value, APPROVAL_FIELDS, APPROVAL_FIELDS);
|
|
69
|
+
try {
|
|
70
|
+
if (!Object.isFrozen(value)) throw new TypeError('Verification input is invalid.');
|
|
71
|
+
} catch {
|
|
72
|
+
throw new TypeError('Verification input is invalid.');
|
|
73
|
+
}
|
|
74
|
+
for (const key of APPROVAL_FIELDS) {
|
|
75
|
+
if (key === 'singleUse') {
|
|
76
|
+
if (typeof approval[key] !== 'boolean') throw new TypeError('Verification input is invalid.');
|
|
77
|
+
} else {
|
|
78
|
+
boundedString(approval[key], key === 'resource' ? 1_024 : 100);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function manualReview(value) {
|
|
85
|
+
const review = snapshot(value, [
|
|
86
|
+
'id', 'acceptanceCriterion', 'expectedApproverId', 'approval',
|
|
87
|
+
], ['id', 'acceptanceCriterion', 'expectedApproverId', 'approval']);
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
id: boundedString(review.id, 64),
|
|
90
|
+
acceptanceCriterion: boundedString(review.acceptanceCriterion, 100),
|
|
91
|
+
expectedApproverId: boundedString(review.expectedApproverId, 64),
|
|
92
|
+
approval: immutableApproval(review.approval),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function callerTest(value) {
|
|
97
|
+
const test = snapshot(value, [
|
|
98
|
+
'id', 'status', 'acceptanceCriteria', 'gateId', 'resultPath', 'resultSha256',
|
|
99
|
+
], ['id', 'status', 'acceptanceCriteria', 'gateId']);
|
|
100
|
+
const result = {
|
|
101
|
+
id: boundedString(test.id, 64),
|
|
102
|
+
status: boundedString(test.status, 32),
|
|
103
|
+
acceptanceCriteria: boundedArray(test.acceptanceCriteria, 128, criterion => boundedString(criterion, 100)),
|
|
104
|
+
gateId: boundedString(test.gateId, 64),
|
|
105
|
+
};
|
|
106
|
+
if (test.resultPath !== undefined) result.resultPath = boundedString(test.resultPath, 500);
|
|
107
|
+
if (test.resultSha256 !== undefined) result.resultSha256 = boundedString(test.resultSha256, 64);
|
|
108
|
+
return Object.freeze(result);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function traceabilityRequest(value) {
|
|
112
|
+
const request = snapshot(value, [
|
|
113
|
+
'subjectId', 'acceptanceCriteria', 'manualReviews', 'tests',
|
|
114
|
+
], ['subjectId', 'acceptanceCriteria', 'manualReviews']);
|
|
115
|
+
return Object.freeze({
|
|
116
|
+
subjectId: boundedString(request.subjectId, 64),
|
|
117
|
+
acceptanceCriteria: boundedArray(request.acceptanceCriteria, 2_000, acceptanceCriterion),
|
|
118
|
+
manualReviews: boundedArray(request.manualReviews, 2_000, manualReview),
|
|
119
|
+
tests: request.tests === undefined
|
|
120
|
+
? Object.freeze([])
|
|
121
|
+
: boundedArray(request.tests, 2_000, callerTest),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function verifyProject(input, options = {}) {
|
|
126
|
+
const request = snapshot(input, ['quality', 'traceability'], ['quality', 'traceability']);
|
|
127
|
+
const configured = snapshot(options, ['now', 'approvalRegistry', 'nowMs', 'gitClient'], ['gitClient']);
|
|
128
|
+
const traceRequest = traceabilityRequest(request.traceability);
|
|
129
|
+
const quality = await runQualityGates(request.quality, {
|
|
130
|
+
gitClient: configured.gitClient,
|
|
131
|
+
...(configured.now === undefined ? {} : { now: configured.now }),
|
|
132
|
+
});
|
|
133
|
+
const traceability = validateTraceability({
|
|
134
|
+
subjectId: traceRequest.subjectId,
|
|
135
|
+
qualityRun: quality,
|
|
136
|
+
acceptanceCriteria: traceRequest.acceptanceCriteria,
|
|
137
|
+
manualReviews: traceRequest.manualReviews,
|
|
138
|
+
}, {
|
|
139
|
+
approvalRegistry: configured.approvalRegistry,
|
|
140
|
+
nowMs: configured.nowMs,
|
|
141
|
+
});
|
|
142
|
+
const gateBindingsValid = true;
|
|
143
|
+
const ok = quality.status === 'pass' && traceability.valid;
|
|
144
|
+
return immutable({
|
|
145
|
+
ok,
|
|
146
|
+
status: ok ? 'pass' : 'fail',
|
|
147
|
+
commitSha: quality.commitSha,
|
|
148
|
+
quality,
|
|
149
|
+
traceability,
|
|
150
|
+
gateBindingsValid,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function verifyCommand(parsed, dependencies = {}) {
|
|
155
|
+
if (!parsed || parsed.command !== 'verify' || parsed.subcommand !== null
|
|
156
|
+
|| !Array.isArray(parsed.operands) || parsed.operands.length !== 0
|
|
157
|
+
|| !parsed.flags || Reflect.ownKeys(parsed.flags).some(key => key !== 'json')) {
|
|
158
|
+
throw new CliError('Verify command options are invalid.', 'INVALID_INPUT');
|
|
159
|
+
}
|
|
160
|
+
const context = dependencies.quality;
|
|
161
|
+
if (!context || typeof context.requestFor !== 'function') {
|
|
162
|
+
throw new CliError('Quality verification is not configured.', 'MISSING_CONFIGURATION');
|
|
163
|
+
}
|
|
164
|
+
const request = await context.requestFor(Object.freeze({ command: 'verify' }));
|
|
165
|
+
const result = await verifyProject(request, context);
|
|
166
|
+
const exitCode = result.ok ? EXIT_CODES.SUCCESS : EXIT_CODES.FAILED_GATE;
|
|
167
|
+
if (parsed.flags.json) dependencies.output.json({ ok: result.ok, command: 'verify', result }, result.ok ? 'stdout' : 'stderr');
|
|
168
|
+
else if (result.ok) dependencies.output.log('Verification: pass.');
|
|
169
|
+
else dependencies.output.error('Verification: fail.');
|
|
170
|
+
return exitCode;
|
|
171
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { parseAcceptanceCriteriaText } from '../work-request/contract.js';
|
|
2
|
+
import * as filesystem from 'node:fs';
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { TextDecoder } from 'node:util';
|
|
5
|
+
|
|
6
|
+
import { immutableJson } from '../clients/contract.js';
|
|
7
|
+
import { CliError, EXIT_CODES } from '../cli/output.js';
|
|
8
|
+
|
|
9
|
+
const RUN_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
10
|
+
const MAX_DECOMPOSITION_BYTES = 128 * 1024;
|
|
11
|
+
const MAX_INLINE_JSON_BYTES = 64 * 1024;
|
|
12
|
+
const SUBCOMMANDS = new Set(['propose', 'prepare', 'next', 'status', 'submit', 'verify', 'recover']);
|
|
13
|
+
|
|
14
|
+
function fail(message, code = 'INVALID_INPUT') { throw new CliError(message, code); }
|
|
15
|
+
|
|
16
|
+
function supplementalCriteria(value) {
|
|
17
|
+
try { return parseAcceptanceCriteriaText(value); }
|
|
18
|
+
catch { fail('Acceptance criteria must be nonempty bounded text, one criterion per line, without duplicates, control characters or credentials.'); }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function absolute(value, label) {
|
|
22
|
+
if (typeof value !== 'string' || value.length < 2 || value.length > 4096 || !isAbsolute(value)
|
|
23
|
+
|| resolve(value) !== value || /[\u0000\r\n]/.test(value)) fail(`${label} is invalid.`);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function runId(value) {
|
|
28
|
+
if (typeof value !== 'string' || value.length > 64 || !RUN_ID.test(value)) fail('Work run ID is invalid.');
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function version(value, label) {
|
|
33
|
+
if (typeof value !== 'string' || !/^[1-9][0-9]*$/.test(value)) fail(`${label} is invalid.`);
|
|
34
|
+
const parsed = Number(value);
|
|
35
|
+
if (!Number.isSafeInteger(parsed)) fail(`${label} is invalid.`);
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function sameFile(left, right) {
|
|
40
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.nlink === right.nlink;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readJson(project, path, fs, label) {
|
|
44
|
+
const target = absolute(path, `${label} path`);
|
|
45
|
+
const contained = relative(project, target);
|
|
46
|
+
if (!contained || contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {
|
|
47
|
+
fail(`${label} must stay within the project.`);
|
|
48
|
+
}
|
|
49
|
+
let ancestor = project;
|
|
50
|
+
for (const part of contained.split(sep).slice(0, -1)) {
|
|
51
|
+
ancestor = join(ancestor, part);
|
|
52
|
+
let status;
|
|
53
|
+
try { status = fs.lstatSync(ancestor); } catch { fail(`${label} has an unsafe ancestor.`, 'REPOSITORY_CONFLICT'); }
|
|
54
|
+
if (status.isSymbolicLink() || !status.isDirectory()) fail(`${label} has an unsafe ancestor.`, 'REPOSITORY_CONFLICT');
|
|
55
|
+
}
|
|
56
|
+
let descriptor;
|
|
57
|
+
try {
|
|
58
|
+
const before = fs.lstatSync(target);
|
|
59
|
+
if (before.isSymbolicLink() || !before.isFile() || before.nlink !== 1
|
|
60
|
+
|| before.size < 1 || before.size > MAX_DECOMPOSITION_BYTES) fail(`${label} must be a bounded regular file.`);
|
|
61
|
+
descriptor = fs.openSync(target, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
62
|
+
const opened = fs.fstatSync(descriptor);
|
|
63
|
+
if (!sameFile(before, opened)) fail(`${label} changed during validation.`, 'REPOSITORY_CONFLICT');
|
|
64
|
+
const bytes = fs.readFileSync(descriptor);
|
|
65
|
+
const after = fs.fstatSync(descriptor);
|
|
66
|
+
if (!sameFile(opened, after) || bytes.byteLength !== after.size) fail(`${label} changed during validation.`, 'REPOSITORY_CONFLICT');
|
|
67
|
+
let source;
|
|
68
|
+
try { source = new TextDecoder('utf-8', { fatal: true }).decode(bytes); } catch { fail(`${label} is not valid UTF-8.`); }
|
|
69
|
+
try { return JSON.parse(source); } catch { fail(`${label} is not valid JSON.`); }
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error instanceof CliError) throw error;
|
|
72
|
+
fail(`${label} could not be read.`, 'REPOSITORY_CONFLICT');
|
|
73
|
+
} finally {
|
|
74
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function inputJson(project, flags, name, fs, label) {
|
|
79
|
+
const inlineName = `${name}-json`;
|
|
80
|
+
const selected = [name, inlineName].filter(key => flags[key] !== undefined);
|
|
81
|
+
if (selected.length !== 1) fail(`${label} requires exactly one of --${name} or --${inlineName}.`);
|
|
82
|
+
if (selected[0] === name) return readJson(project, flags[name], fs, label);
|
|
83
|
+
const source = flags[inlineName];
|
|
84
|
+
if (typeof source !== 'string' || !source.length || Buffer.byteLength(source, 'utf8') > MAX_INLINE_JSON_BYTES) {
|
|
85
|
+
fail(`${label} inline JSON must be between 1 and ${MAX_INLINE_JSON_BYTES} UTF-8 bytes. Use --${name} for larger input.`);
|
|
86
|
+
}
|
|
87
|
+
try { return JSON.parse(source); } catch { fail(`${label} is not valid JSON.`); }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function service(dependencies, name, method) {
|
|
91
|
+
const value = dependencies?.[name];
|
|
92
|
+
if (!value || typeof value !== 'object' || typeof value[method] !== 'function') {
|
|
93
|
+
fail('Host work execution is not configured.', 'MISSING_CONFIGURATION');
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function invoke(target, method, input) {
|
|
99
|
+
try { return await target[method](input); }
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error instanceof CliError) throw error;
|
|
102
|
+
const code = String(error?.code ?? '');
|
|
103
|
+
if (code === 'EPERM' || code === 'EACCES') {
|
|
104
|
+
throw new CliError('Rivet lacks filesystem permission for this host operation. Request approval for this exact command through your harness, including private Git state and isolated worktree access. If approval is unavailable, use an approved interactive session or the terminal workflow.', 'REPOSITORY_CONFLICT', { cause: error });
|
|
105
|
+
}
|
|
106
|
+
const publicCode = code === 'ERR_INTEGRATION_INPUT' ? 'INVALID_INPUT'
|
|
107
|
+
: code.startsWith('ERR_INTEGRATION_') ? 'PROVIDER_UNAVAILABLE'
|
|
108
|
+
: code === 'ERR_HOST_EXECUTION_VERIFICATION_FAILED' ? 'FAILED_GATE'
|
|
109
|
+
: code === 'ERR_HOST_EXECUTION_REPOSITORY' || code.startsWith('ERR_HOST_RUN_') || code.includes('STATE_CONFLICT') || code.includes('VERSION') || code.startsWith('ERR_GIT_')
|
|
110
|
+
? 'REPOSITORY_CONFLICT'
|
|
111
|
+
: code.includes('INVALID') ? 'INVALID_INPUT'
|
|
112
|
+
: code.includes('MISSING') || code.includes('CONFIGURATION') ? 'MISSING_CONFIGURATION' : 'INTERNAL_ERROR';
|
|
113
|
+
throw new CliError(error?.safeMessage ?? 'Host work execution failed.', publicCode, { cause: error });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function emit(parsed, dependencies, result) {
|
|
118
|
+
const safe = immutableJson(result);
|
|
119
|
+
if (parsed.flags.json) dependencies.output.json({ ok: true, command: 'work', subcommand: parsed.subcommand, result: safe });
|
|
120
|
+
else dependencies.output.log(JSON.stringify(safe, null, 2));
|
|
121
|
+
return EXIT_CODES.SUCCESS;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function proposalInput(parsed, dependencies) {
|
|
125
|
+
if (parsed.operands.length !== 0) fail('Work propose does not accept a run ID.');
|
|
126
|
+
const project = absolute(parsed.flags.project, 'Work project');
|
|
127
|
+
const selectors = ['request', 'request-text', 'ticket', 'host-context-json'].filter(key => parsed.flags[key] !== undefined);
|
|
128
|
+
if (selectors.length !== 1) {
|
|
129
|
+
fail('Work propose requires one request source.');
|
|
130
|
+
}
|
|
131
|
+
const selected = selectors[0];
|
|
132
|
+
let source;
|
|
133
|
+
if (selected === 'request') source = { kind: 'file', value: absolute(parsed.flags.request, 'Work request path') };
|
|
134
|
+
else if (selected === 'request-text') source = { kind: 'inline', value: parsed.flags['request-text'] };
|
|
135
|
+
else if (selected === 'host-context-json') {
|
|
136
|
+
source = { kind: 'host-observation', value: inputJson(project, parsed.flags, 'host-context', dependencies.fs ?? filesystem, 'Host context') };
|
|
137
|
+
} else source = { kind: 'ticket', value: parsed.flags.ticket };
|
|
138
|
+
if (selected !== 'ticket' && parsed.flags['acceptance-criteria'] !== undefined) fail('--acceptance-criteria requires --ticket.');
|
|
139
|
+
if (selected !== 'ticket' && parsed.flags.tracker !== undefined) fail('--tracker is only valid with --ticket.');
|
|
140
|
+
return Object.freeze({
|
|
141
|
+
project,
|
|
142
|
+
source: Object.freeze(source),
|
|
143
|
+
client: 'host',
|
|
144
|
+
...(parsed.flags['acceptance-criteria'] === undefined ? {} : { userAcceptanceCriteria: supplementalCriteria(parsed.flags['acceptance-criteria']) }),
|
|
145
|
+
decomposition: inputJson(project, parsed.flags, 'decomposition', dependencies.fs ?? filesystem, 'Work decomposition'),
|
|
146
|
+
...(parsed.flags.tracker === undefined ? {} : { tracker: parsed.flags.tracker }),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function lifecycleInput(parsed, versionFlag = null) {
|
|
151
|
+
const hasRunFlag = parsed.flags.run !== undefined;
|
|
152
|
+
if (parsed.operands.length !== (hasRunFlag ? 0 : 1)) fail('Use one run selector: --run or a positional run ID.');
|
|
153
|
+
const input = { project: absolute(parsed.flags.project, 'Work project'), runId: runId(hasRunFlag ? parsed.flags.run : parsed.operands[0]) };
|
|
154
|
+
if (versionFlag) input[versionFlag.output] = version(parsed.flags[versionFlag.flag], `--${versionFlag.flag}`);
|
|
155
|
+
return Object.freeze(input);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function workCommand(parsed, dependencies) {
|
|
159
|
+
if (!parsed || parsed.command !== 'work' || !SUBCOMMANDS.has(parsed.subcommand)
|
|
160
|
+
|| !Array.isArray(parsed.operands) || !parsed.flags || typeof parsed.flags !== 'object') fail('Work command is invalid.');
|
|
161
|
+
const allowedFlags = {
|
|
162
|
+
propose: ['project', 'request', 'request-text', 'host-context-json', 'ticket', 'tracker', 'acceptance-criteria', 'decomposition', 'decomposition-json', 'json'],
|
|
163
|
+
prepare: ['project', 'expected-version', 'json'],
|
|
164
|
+
next: ['project', 'expected-runtime-version', 'json'],
|
|
165
|
+
status: ['project', 'json'],
|
|
166
|
+
recover: ['project', 'json'],
|
|
167
|
+
submit: ['project', 'expected-runtime-version', 'action', 'action-json', 'result', 'result-json', 'json'],
|
|
168
|
+
verify: ['project', 'expected-version', 'expected-runtime-version', 'json'],
|
|
169
|
+
};
|
|
170
|
+
if (parsed.subcommand !== 'propose') allowedFlags[parsed.subcommand].push('run');
|
|
171
|
+
if (Object.keys(parsed.flags).some(key => !allowedFlags[parsed.subcommand].includes(key))) {
|
|
172
|
+
fail(`Work ${parsed.subcommand} options are invalid.`);
|
|
173
|
+
}
|
|
174
|
+
if (parsed.subcommand === 'recover') {
|
|
175
|
+
const result = immutableJson(await invoke(service(dependencies, 'work', 'recover'), 'recover', lifecycleInput(parsed)));
|
|
176
|
+
const ok = result.status === 'recovered';
|
|
177
|
+
if (parsed.flags.json) dependencies.output.json({ ok, command: 'work', subcommand: 'recover', result }, ok ? 'stdout' : 'stderr');
|
|
178
|
+
else (ok ? dependencies.output.log : dependencies.output.error)(JSON.stringify(result, null, 2));
|
|
179
|
+
return ok ? EXIT_CODES.SUCCESS : EXIT_CODES.REPOSITORY_CONFLICT;
|
|
180
|
+
}
|
|
181
|
+
if (parsed.subcommand === 'propose') {
|
|
182
|
+
return emit(parsed, dependencies, await invoke(
|
|
183
|
+
service(dependencies, 'feature', 'propose'), 'propose', proposalInput(parsed, dependencies),
|
|
184
|
+
));
|
|
185
|
+
}
|
|
186
|
+
if (parsed.subcommand === 'prepare') {
|
|
187
|
+
return emit(parsed, dependencies, await invoke(service(dependencies, 'work', 'prepare'), 'prepare', lifecycleInput(
|
|
188
|
+
parsed, { flag: 'expected-version', output: 'expectedRunVersion' },
|
|
189
|
+
)));
|
|
190
|
+
}
|
|
191
|
+
if (parsed.subcommand === 'next') {
|
|
192
|
+
return emit(parsed, dependencies, await invoke(service(dependencies, 'work', 'nextAction'), 'nextAction', lifecycleInput(
|
|
193
|
+
parsed, { flag: 'expected-runtime-version', output: 'expectedRuntimeVersion' },
|
|
194
|
+
)));
|
|
195
|
+
}
|
|
196
|
+
if (parsed.subcommand === 'status') {
|
|
197
|
+
return emit(parsed, dependencies, await invoke(
|
|
198
|
+
service(dependencies, 'work', 'status'), 'status', lifecycleInput(parsed),
|
|
199
|
+
));
|
|
200
|
+
}
|
|
201
|
+
if (parsed.subcommand === 'submit') {
|
|
202
|
+
const input = lifecycleInput(parsed, { flag: 'expected-runtime-version', output: 'expectedRuntimeVersion' });
|
|
203
|
+
const fs = dependencies.fs ?? filesystem;
|
|
204
|
+
return emit(parsed, dependencies, await invoke(service(dependencies, 'work', 'submitResult'), 'submitResult', Object.freeze({
|
|
205
|
+
...input,
|
|
206
|
+
action: inputJson(input.project, parsed.flags, 'action', fs, 'Work action'),
|
|
207
|
+
result: inputJson(input.project, parsed.flags, 'result', fs, 'Work result'),
|
|
208
|
+
})));
|
|
209
|
+
}
|
|
210
|
+
if (parsed.subcommand === 'verify') {
|
|
211
|
+
const input = lifecycleInput(parsed, { flag: 'expected-version', output: 'expectedRunVersion' });
|
|
212
|
+
return emit(parsed, dependencies, await invoke(service(dependencies, 'work', 'verify'), 'verify', Object.freeze({
|
|
213
|
+
...input,
|
|
214
|
+
expectedRuntimeVersion: version(parsed.flags['expected-runtime-version'], '--expected-runtime-version'),
|
|
215
|
+
})));
|
|
216
|
+
}
|
|
217
|
+
fail(`Work ${parsed.subcommand} is invalid.`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export { MAX_DECOMPOSITION_BYTES };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createReservedWorktree, recoverStaleWorktree, verifyReservedWorktree } from '../git/worktrees.js';
|
|
2
|
+
import { reconcileWorktree } from '../git/reconcile.js';
|
|
3
|
+
|
|
4
|
+
function captureRequest(input) {
|
|
5
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new TypeError('Invalid worktree command request');
|
|
6
|
+
const keys = Reflect.ownKeys(input);
|
|
7
|
+
if (keys.some(key => typeof key !== 'string')) throw new TypeError('Invalid worktree command request');
|
|
8
|
+
const result = Object.create(null);
|
|
9
|
+
try {
|
|
10
|
+
for (const key of keys) {
|
|
11
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
12
|
+
if (!descriptor?.enumerable) throw new TypeError('Invalid worktree command request');
|
|
13
|
+
result[key] = input[key];
|
|
14
|
+
}
|
|
15
|
+
} catch {
|
|
16
|
+
throw new TypeError('Invalid worktree command request');
|
|
17
|
+
}
|
|
18
|
+
if (typeof result.action !== 'string') throw new TypeError('Invalid worktree command request');
|
|
19
|
+
return Object.freeze(result);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runWorktreeCommand(input, dependencies = {}) {
|
|
23
|
+
const request = captureRequest(input);
|
|
24
|
+
const options = Object.freeze({
|
|
25
|
+
gitClient: dependencies.gitClient,
|
|
26
|
+
nowMs: dependencies.nowMs,
|
|
27
|
+
});
|
|
28
|
+
if (request.action === 'create') {
|
|
29
|
+
const { action: _action, ...payload } = request;
|
|
30
|
+
return createReservedWorktree(payload, options);
|
|
31
|
+
}
|
|
32
|
+
if (request.action === 'inspect') {
|
|
33
|
+
const { action: _action, ...payload } = request;
|
|
34
|
+
return verifyReservedWorktree(payload, options);
|
|
35
|
+
}
|
|
36
|
+
if (request.action === 'reconcile') {
|
|
37
|
+
const { action: _action, ...payload } = request;
|
|
38
|
+
return reconcileWorktree(payload, options);
|
|
39
|
+
}
|
|
40
|
+
if (request.action === 'recover') {
|
|
41
|
+
const { action: _action, ...payload } = request;
|
|
42
|
+
return recoverStaleWorktree(payload, dependencies);
|
|
43
|
+
}
|
|
44
|
+
throw new TypeError('Invalid worktree command action');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatWorktreeResult(result, options = {}) {
|
|
48
|
+
if (!result || typeof result !== 'object' || Array.isArray(result)) throw new TypeError('Invalid worktree command result');
|
|
49
|
+
if (options.json === true) return JSON.stringify(result);
|
|
50
|
+
const status = typeof result.status === 'string' ? result.status : result.reservation?.status;
|
|
51
|
+
return `Worktree: ${status ?? 'unknown'}.`;
|
|
52
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import * as filesystem from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { commandToolKey, compileProjectDependencies, compileQualitySteps } from './commands.js';
|
|
5
|
+
|
|
6
|
+
const MAX_MANIFEST_BYTES = 256 * 1024;
|
|
7
|
+
|
|
8
|
+
function sameIdentity(left, right) {
|
|
9
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function within(root, candidate) {
|
|
13
|
+
const path = relative(root, candidate);
|
|
14
|
+
return path === '' || (!isAbsolute(path) && path !== '..' && !path.startsWith(`..${sep}`));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function runnerManager(runner) {
|
|
18
|
+
return runner.toLowerCase().replace(/\.(?:cmd|exe)$/, '');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function directoryFor(root, cwd, fs) {
|
|
22
|
+
let current = root;
|
|
23
|
+
for (const component of cwd === '.' ? [] : cwd.split('/')) {
|
|
24
|
+
current = join(current, component);
|
|
25
|
+
const before = fs.lstatSync(current, { throwIfNoEntry: false });
|
|
26
|
+
if (!before?.isDirectory() || before.isSymbolicLink()) return { status: 'unsafe-directory' };
|
|
27
|
+
const canonical = fs.realpathSync(current);
|
|
28
|
+
const after = fs.statSync(canonical);
|
|
29
|
+
if (!within(root, canonical)) return { status: 'unsafe-directory' };
|
|
30
|
+
if (!sameIdentity(before, after)) return { status: 'directory-identity-changed' };
|
|
31
|
+
}
|
|
32
|
+
const metadata = fs.lstatSync(current, { throwIfNoEntry: false });
|
|
33
|
+
if (!metadata?.isDirectory() || metadata.isSymbolicLink()) return { status: 'unsafe-directory' };
|
|
34
|
+
const canonical = fs.realpathSync(current);
|
|
35
|
+
const canonicalMetadata = fs.statSync(canonical);
|
|
36
|
+
if (!within(root, canonical)) return { status: 'unsafe-directory' };
|
|
37
|
+
if (!sameIdentity(metadata, canonicalMetadata)) return { status: 'directory-identity-changed' };
|
|
38
|
+
return { status: 'ready', path: canonical, identity: metadata };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function directoryIdentityStatus(directory, fs) {
|
|
42
|
+
try {
|
|
43
|
+
const current = fs.lstatSync(directory.path, { throwIfNoEntry: false });
|
|
44
|
+
if (!current?.isDirectory() || current.isSymbolicLink()
|
|
45
|
+
|| !sameIdentity(directory.identity, current)) return 'directory-identity-changed';
|
|
46
|
+
const canonical = fs.realpathSync(directory.path);
|
|
47
|
+
const canonicalMetadata = fs.statSync(canonical);
|
|
48
|
+
return canonical === directory.path && sameIdentity(directory.identity, canonicalMetadata)
|
|
49
|
+
? 'ready' : 'directory-identity-changed';
|
|
50
|
+
} catch {
|
|
51
|
+
return 'directory-identity-changed';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readManifest(directory, fs) {
|
|
56
|
+
const path = join(directory.path, 'package.json');
|
|
57
|
+
const before = fs.lstatSync(path, { throwIfNoEntry: false });
|
|
58
|
+
if (!before) return { status: 'missing-manifest' };
|
|
59
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_MANIFEST_BYTES) {
|
|
60
|
+
return { status: 'unsafe-manifest' };
|
|
61
|
+
}
|
|
62
|
+
let descriptor;
|
|
63
|
+
try {
|
|
64
|
+
descriptor = fs.openSync(path, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
65
|
+
const opened = fs.fstatSync(descriptor);
|
|
66
|
+
if (!opened.isFile() || !sameIdentity(before, opened) || opened.size > MAX_MANIFEST_BYTES) {
|
|
67
|
+
return { status: 'manifest-identity-changed' };
|
|
68
|
+
}
|
|
69
|
+
const bytes = Buffer.alloc(MAX_MANIFEST_BYTES + 1);
|
|
70
|
+
let offset = 0;
|
|
71
|
+
while (offset < bytes.length) {
|
|
72
|
+
const count = fs.readSync(descriptor, bytes, offset, bytes.length - offset, null);
|
|
73
|
+
if (!Number.isSafeInteger(count) || count < 0 || count > bytes.length - offset) return { status: 'unsafe-manifest' };
|
|
74
|
+
if (count === 0) break;
|
|
75
|
+
offset += count;
|
|
76
|
+
}
|
|
77
|
+
if (offset > MAX_MANIFEST_BYTES) return { status: 'unsafe-manifest' };
|
|
78
|
+
const after = fs.fstatSync(descriptor);
|
|
79
|
+
const current = fs.lstatSync(path, { throwIfNoEntry: false });
|
|
80
|
+
if (!current?.isFile() || current.isSymbolicLink() || !sameIdentity(opened, after)
|
|
81
|
+
|| !sameIdentity(after, current) || after.size !== current.size) return { status: 'manifest-identity-changed' };
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, offset)));
|
|
85
|
+
} catch { return { status: 'invalid-manifest' }; }
|
|
86
|
+
return { status: 'ready', manifest: parsed };
|
|
87
|
+
} catch {
|
|
88
|
+
return { status: 'unsafe-manifest' };
|
|
89
|
+
} finally {
|
|
90
|
+
if (descriptor !== undefined) {
|
|
91
|
+
try { fs.closeSync(descriptor); } catch {}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function frozen(value) {
|
|
97
|
+
if (Array.isArray(value)) return Object.freeze(value.map(frozen));
|
|
98
|
+
if (value && typeof value === 'object') {
|
|
99
|
+
return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, child]) => [key, frozen(child)])));
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function localExecutableStatus(root, cwd, runner, fs) {
|
|
105
|
+
if (!runner.startsWith('./')) return 'external';
|
|
106
|
+
let current = root;
|
|
107
|
+
const parts = [...(cwd === '.' ? [] : cwd.split('/')), ...runner.slice(2).split('/')];
|
|
108
|
+
try {
|
|
109
|
+
for (const part of parts.slice(0, -1)) {
|
|
110
|
+
current = join(current, part);
|
|
111
|
+
const metadata = fs.lstatSync(current, { throwIfNoEntry: false });
|
|
112
|
+
if (!metadata) return 'missing';
|
|
113
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink() || fs.realpathSync(current) !== current) return 'unsafe';
|
|
114
|
+
}
|
|
115
|
+
return fs.lstatSync(join(current, parts.at(-1)), { throwIfNoEntry: false }) ? 'exists' : 'missing';
|
|
116
|
+
} catch { return 'unsafe'; }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function providedExecutable(dependencies, cwd, runner) {
|
|
120
|
+
return runner.startsWith('./') && dependencies?.provides.includes(`./${cwd === '.' ? '' : `${cwd}/`}${runner.slice(2)}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function dependencyReadiness(root, rootSafe, dependencies, tools, fs) {
|
|
124
|
+
if (!dependencies) return undefined;
|
|
125
|
+
const inputs = dependencies.inputs.map(path => {
|
|
126
|
+
let status = 'unsafe-input';
|
|
127
|
+
try {
|
|
128
|
+
const directory = directoryFor(root, dirname(path), fs);
|
|
129
|
+
const file = join(root, path);
|
|
130
|
+
const metadata = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
131
|
+
if (!metadata) status = 'missing-input';
|
|
132
|
+
else if (rootSafe && directory.status === 'ready' && metadata.isFile() && !metadata.isSymbolicLink()
|
|
133
|
+
&& metadata.nlink === 1 && metadata.size <= 4 * 1024 * 1024 && fs.realpathSync(file) === file
|
|
134
|
+
&& sameIdentity(metadata, fs.statSync(file)) && directoryIdentityStatus(directory, fs) === 'ready') status = 'ready';
|
|
135
|
+
} catch {}
|
|
136
|
+
return { path, status };
|
|
137
|
+
});
|
|
138
|
+
let precedingStep = false;
|
|
139
|
+
const steps = dependencies.steps.map(step => {
|
|
140
|
+
let status = rootSafe ? directoryFor(root, step.cwd, fs).status : 'unsafe-directory';
|
|
141
|
+
if (status === 'ready') {
|
|
142
|
+
if (tools[commandToolKey(step.argv[0], step.cwd)]?.runtimeResolved === true) status = 'ready';
|
|
143
|
+
else if (precedingStep && providedExecutable(dependencies, step.cwd, step.argv[0])
|
|
144
|
+
&& localExecutableStatus(root, step.cwd, step.argv[0], fs) === 'missing') status = 'preparation-required';
|
|
145
|
+
else status = 'tool-unavailable';
|
|
146
|
+
}
|
|
147
|
+
if (status === 'ready') precedingStep = true;
|
|
148
|
+
return { ...step, status, available: status === 'ready' };
|
|
149
|
+
});
|
|
150
|
+
const preparationReady = rootSafe && inputs.every(input => input.status === 'ready')
|
|
151
|
+
&& steps.every(step => ['ready', 'preparation-required'].includes(step.status));
|
|
152
|
+
return { preparationReady, inputs, steps };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function inspectCommandReadiness(projectRoot, config, options = {}) {
|
|
156
|
+
const fs = options.fs ?? filesystem;
|
|
157
|
+
const tools = options.tools ?? {};
|
|
158
|
+
const lexicalRoot = resolve(projectRoot);
|
|
159
|
+
const rootMetadata = fs.lstatSync(lexicalRoot, { throwIfNoEntry: false });
|
|
160
|
+
const root = rootMetadata?.isDirectory() && !rootMetadata.isSymbolicLink()
|
|
161
|
+
? fs.realpathSync(lexicalRoot) : lexicalRoot;
|
|
162
|
+
const rootSafe = rootMetadata?.isDirectory() && !rootMetadata.isSymbolicLink()
|
|
163
|
+
&& sameIdentity(rootMetadata, fs.statSync(root));
|
|
164
|
+
const expectedManager = config.project.stack.packageManager;
|
|
165
|
+
const dependencies = compileProjectDependencies(config.project);
|
|
166
|
+
const dependencyStatus = dependencyReadiness(root, rootSafe, dependencies, tools, fs);
|
|
167
|
+
const steps = compileQualitySteps(config).map(step => {
|
|
168
|
+
const direct = step.execution === 'argv';
|
|
169
|
+
const manager = direct ? step.argv[0] : runnerManager(step.argv[0]);
|
|
170
|
+
let status = 'ready';
|
|
171
|
+
if (!rootSafe) status = 'unsafe-directory';
|
|
172
|
+
else if (config.project.schemaVersion === 2 && manager !== expectedManager) status = 'manager-mismatch';
|
|
173
|
+
else if (direct ? tools[commandToolKey(manager, step.cwd)]?.runtimeResolved !== true
|
|
174
|
+
: tools[manager]?.present !== true || tools[manager]?.supported !== true
|
|
175
|
+
|| tools[manager]?.runtimeResolved === false) status = 'tool-unavailable';
|
|
176
|
+
else {
|
|
177
|
+
const directory = directoryFor(root, step.cwd, fs);
|
|
178
|
+
status = directory.status;
|
|
179
|
+
if (status === 'ready') {
|
|
180
|
+
status = directoryIdentityStatus(directory, fs);
|
|
181
|
+
const manifest = status === 'ready' && !direct ? readManifest(directory, fs) : null;
|
|
182
|
+
if (manifest) status = manifest.status;
|
|
183
|
+
const finalDirectoryStatus = directoryIdentityStatus(directory, fs);
|
|
184
|
+
if (finalDirectoryStatus !== 'ready') status = finalDirectoryStatus;
|
|
185
|
+
if (status === 'ready' && manifest) {
|
|
186
|
+
const scripts = manifest.manifest?.scripts;
|
|
187
|
+
status = scripts && typeof scripts === 'object' && !Array.isArray(scripts)
|
|
188
|
+
&& typeof scripts[step.argv[2]] === 'string' ? 'ready' : 'missing-script';
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (direct && status === 'tool-unavailable' && dependencyStatus?.preparationReady
|
|
193
|
+
&& directoryFor(root, step.cwd, fs).status === 'ready'
|
|
194
|
+
&& providedExecutable(dependencies, step.cwd, manager)
|
|
195
|
+
&& localExecutableStatus(root, step.cwd, manager, fs) === 'missing') status = 'preparation-required';
|
|
196
|
+
return {
|
|
197
|
+
id: step.id,
|
|
198
|
+
logicalId: step.logicalId,
|
|
199
|
+
cwd: step.cwd,
|
|
200
|
+
argv: step.argv,
|
|
201
|
+
script: direct ? null : step.argv[2],
|
|
202
|
+
manager,
|
|
203
|
+
required: step.required,
|
|
204
|
+
status,
|
|
205
|
+
available: status === 'ready',
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
return frozen({
|
|
209
|
+
ready: steps.filter(step => step.required).every(step => step.status === 'ready'),
|
|
210
|
+
allReady: steps.every(step => step.status === 'ready'),
|
|
211
|
+
preparationReady: steps.filter(step => step.required).every(step => ['ready', 'preparation-required'].includes(step.status))
|
|
212
|
+
&& (!dependencyStatus || dependencyStatus.preparationReady),
|
|
213
|
+
...(dependencyStatus ? { dependencies: dependencyStatus } : {}),
|
|
214
|
+
steps,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export { MAX_MANIFEST_BYTES as MAX_COMMAND_MANIFEST_BYTES };
|