@fitlab-ai/agent-infra 0.9.14 → 0.10.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/README.md +10 -0
- package/README.zh-CN.md +10 -0
- package/bin/internal-cli.sh +45 -0
- package/dist/bin/internal-cli.js +93 -30
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +9 -4
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
- package/dist/lib/defaults.json +103 -103
- package/dist/lib/fs/durable-write.js +39 -0
- package/dist/lib/fs/file-lock.js +111 -0
- package/dist/lib/host-control/audit.js +19 -0
- package/dist/lib/host-control/client.js +142 -0
- package/dist/lib/host-control/command.js +49 -0
- package/dist/lib/host-control/path.js +160 -0
- package/dist/lib/host-control/secure-fs.js +140 -0
- package/dist/lib/host-control/server.js +159 -0
- package/dist/lib/host-control/service.js +112 -0
- package/dist/lib/init.js +11 -2
- package/dist/lib/internal/cli-route-inventory.js +4 -2
- package/dist/lib/internal/codex-lifecycle.js +6 -6
- package/dist/lib/internal/demo-promotion.js +322 -0
- package/dist/lib/internal/demo-transcript.js +454 -0
- package/dist/lib/internal/host-control.js +28 -0
- package/dist/lib/internal/manual-validation.js +331 -0
- package/dist/lib/internal/platform-pr.js +18 -4
- package/dist/lib/internal/release-workflow.js +63 -84
- package/dist/lib/internal/sandbox-control.js +149 -2
- package/dist/lib/internal/task-artifact.js +16 -263
- package/dist/lib/internal/task-create.js +6 -5
- package/dist/lib/internal/task-event.js +37 -37
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-operation-registry.js +101 -4
- package/dist/lib/internal/task-review.js +11 -56
- package/dist/lib/internal/task-validate.js +30 -10
- package/dist/lib/owned-path.js +33 -0
- package/dist/lib/platform/check-status.js +11 -0
- package/dist/lib/platform/comment-safety.js +4 -1
- package/dist/lib/platform/completion-backfill.js +1 -1
- package/dist/lib/platform/github-data.js +241 -0
- package/dist/lib/platform/github-provider.js +15 -20
- package/dist/lib/platform/issue-comments.js +18 -12
- package/dist/lib/platform/issues.js +2 -180
- package/dist/lib/platform/pr-change-report.js +5 -4
- package/dist/lib/platform/pr-checks.js +12 -65
- package/dist/lib/platform/pr-summary.js +117 -33
- package/dist/lib/platform/pull-requests.js +5 -108
- package/dist/lib/platform/snapshots.js +2 -0
- package/dist/lib/platform/task-identities.js +2 -9
- package/dist/lib/platform/verification-sync.js +132 -143
- package/dist/lib/pr-review/evidence-grading.js +4 -8
- package/dist/lib/release/workflow.js +1 -1
- package/dist/lib/sandbox/commands/create.js +7 -2
- package/dist/lib/sandbox/commands/rm.js +19 -1511
- package/dist/lib/sandbox/control/audit.js +203 -0
- package/dist/lib/sandbox/control/client.js +93 -54
- package/dist/lib/sandbox/control/executor.js +129 -65
- package/dist/lib/sandbox/control/finalization-response.js +27 -0
- package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
- package/dist/lib/sandbox/control/lifecycle.js +36 -2
- package/dist/lib/sandbox/control/native-file-lock.js +6 -98
- package/dist/lib/sandbox/control/protocol.js +55 -4
- package/dist/lib/sandbox/control/server.js +518 -127
- package/dist/lib/sandbox/control/state.js +64 -0
- package/dist/lib/sandbox/control/task-workflow.js +74 -0
- package/dist/lib/sandbox/control/workflow-executor.js +89 -0
- package/dist/lib/sandbox/index.js +1 -2
- package/dist/lib/sandbox/recovery.js +28 -4
- package/dist/lib/sandbox/removal.js +822 -0
- package/dist/lib/sandbox/task-cleanup.js +217 -0
- package/dist/lib/sandbox/workspace-identity.js +17 -1
- package/dist/lib/sandbox/workspace-view.js +13 -14
- package/dist/lib/task/activity-intent.js +2 -1
- package/dist/lib/task/activity-log.js +30 -24
- package/dist/lib/task/artifact-command.js +79 -0
- package/dist/lib/task/artifact-lifecycle.js +30 -28
- package/dist/lib/task/artifact-name.js +36 -0
- package/dist/lib/task/artifact-operations.js +29 -289
- package/dist/lib/task/artifact-receipts.js +3 -15
- package/dist/lib/task/artifact-recovery.js +430 -0
- package/dist/lib/task/artifact-repair-intent.js +134 -29
- package/dist/lib/task/artifact-schema.js +6 -14
- package/dist/lib/task/capabilities.js +12 -19
- package/dist/lib/task/codex-orchestration.js +71 -25
- package/dist/lib/task/commit-identity.js +28 -0
- package/dist/lib/task/commit-operation.js +33 -20
- package/dist/lib/task/control-authority.js +403 -6
- package/dist/lib/task/control-recovery.js +140 -0
- package/dist/lib/task/create-service.js +1 -8
- package/dist/lib/task/create.js +62 -9
- package/dist/lib/task/decision-details.js +2 -46
- package/dist/lib/task/delegation-receipts.js +64 -3
- package/dist/lib/task/events.js +275 -22
- package/dist/lib/task/finalization-state.js +9 -0
- package/dist/lib/task/human-override.js +8 -14
- package/dist/lib/task/implementation-inputs.js +3 -3
- package/dist/lib/task/intermediate-cleanup.js +441 -0
- package/dist/lib/task/invalidation.js +18 -65
- package/dist/lib/task/ledger-intents.js +7 -22
- package/dist/lib/task/ledger.js +1 -3
- package/dist/lib/task/lifecycle-recovery.js +549 -0
- package/dist/lib/task/lifecycle.js +31 -4
- package/dist/lib/task/local-artifact-finalization.js +197 -197
- package/dist/lib/task/manual-validation-completion.js +26 -0
- package/dist/lib/task/manual-validation-evidence.js +130 -0
- package/dist/lib/task/manual-validation-receipt.js +105 -0
- package/dist/lib/task/manual-validation-shared.js +40 -0
- package/dist/lib/task/manual-validation-transaction.js +243 -0
- package/dist/lib/task/markdown.js +47 -0
- package/dist/lib/task/orchestration.js +66 -19
- package/dist/lib/task/pr-delivery-fact.js +12 -56
- package/dist/lib/task/qualification-audit.js +20 -34
- package/dist/lib/task/recovery-warning.js +19 -0
- package/dist/lib/task/review-artifacts.js +2 -18
- package/dist/lib/task/review-command.js +26 -0
- package/dist/lib/task/review-finalization.js +175 -216
- package/dist/lib/task/review-fingerprint.js +2 -2
- package/dist/lib/task/sections.js +34 -56
- package/dist/lib/task/short-id.js +15 -7
- package/dist/lib/task/task-execution-lock.js +1 -1
- package/dist/lib/task/verification-engine.js +119 -337
- package/dist/lib/task/verification.js +1 -1
- package/dist/lib/task/workflow-command.js +16 -0
- package/dist/lib/task/workflow-warning-intents.js +0 -16
- package/dist/lib/task/workflow-warnings.js +28 -2
- package/dist/lib/update.js +2 -0
- package/dist/lib/version.js +1 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +10 -4
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
- package/lib/fs/durable-write.ts +30 -0
- package/lib/fs/file-lock.ts +113 -0
- package/lib/host-control/audit.ts +22 -0
- package/lib/host-control/client.ts +172 -0
- package/lib/host-control/command.ts +60 -0
- package/lib/host-control/path.ts +167 -0
- package/lib/host-control/secure-fs.ts +135 -0
- package/lib/host-control/server.ts +180 -0
- package/lib/host-control/service.ts +133 -0
- package/lib/init.ts +15 -2
- package/lib/internal/cli-route-inventory.ts +4 -2
- package/lib/internal/codex-lifecycle.ts +6 -6
- package/lib/internal/demo-promotion.ts +354 -0
- package/lib/internal/demo-transcript.ts +419 -0
- package/lib/internal/host-control.ts +28 -0
- package/lib/internal/manual-validation.ts +315 -0
- package/lib/internal/platform-pr.ts +15 -4
- package/lib/internal/release-workflow.ts +74 -82
- package/lib/internal/sandbox-control.ts +162 -1
- package/lib/internal/task-artifact.ts +14 -217
- package/lib/internal/task-create.ts +5 -5
- package/lib/internal/task-event.ts +20 -11
- package/lib/internal/task-lifecycle.ts +4 -3
- package/lib/internal/task-operation-registry.ts +111 -4
- package/lib/internal/task-review.ts +9 -54
- package/lib/internal/task-validate.ts +26 -10
- package/lib/owned-path.ts +26 -0
- package/lib/platform/check-status.ts +9 -0
- package/lib/platform/comment-safety.ts +5 -0
- package/lib/platform/completion-backfill.ts +1 -1
- package/lib/platform/github-data.ts +322 -0
- package/lib/platform/github-provider.ts +15 -20
- package/lib/platform/issue-comments.ts +21 -14
- package/lib/platform/issues.ts +5 -246
- package/lib/platform/pr-change-report.ts +7 -5
- package/lib/platform/pr-checks.ts +13 -77
- package/lib/platform/pr-summary.ts +123 -35
- package/lib/platform/pull-requests.ts +6 -173
- package/lib/platform/snapshots.ts +53 -0
- package/lib/platform/task-identities.ts +3 -14
- package/lib/platform/verification-sync.ts +150 -156
- package/lib/pr-review/evidence-grading.ts +4 -9
- package/lib/release/workflow.ts +3 -3
- package/lib/sandbox/commands/create.ts +7 -6
- package/lib/sandbox/commands/rm.ts +21 -1895
- package/lib/sandbox/control/audit.ts +253 -0
- package/lib/sandbox/control/client.ts +109 -66
- package/lib/sandbox/control/executor.ts +134 -74
- package/lib/sandbox/control/finalization-response.ts +29 -0
- package/lib/sandbox/control/identity-sentinel.ts +137 -0
- package/lib/sandbox/control/lifecycle.ts +45 -3
- package/lib/sandbox/control/native-file-lock.ts +6 -107
- package/lib/sandbox/control/protocol.ts +80 -6
- package/lib/sandbox/control/server.ts +567 -132
- package/lib/sandbox/control/state.ts +86 -0
- package/lib/sandbox/control/task-workflow.ts +95 -0
- package/lib/sandbox/control/workflow-executor.ts +92 -0
- package/lib/sandbox/index.ts +1 -2
- package/lib/sandbox/recovery.ts +28 -5
- package/lib/sandbox/removal.ts +1142 -0
- package/lib/sandbox/task-cleanup.ts +209 -0
- package/lib/sandbox/workspace-identity.ts +19 -3
- package/lib/sandbox/workspace-view.ts +17 -15
- package/lib/task/activity-intent.ts +2 -2
- package/lib/task/activity-log.ts +27 -19
- package/lib/task/artifact-command.ts +85 -0
- package/lib/task/artifact-lifecycle.ts +31 -26
- package/lib/task/artifact-name.ts +55 -0
- package/lib/task/artifact-operations.ts +34 -310
- package/lib/task/artifact-receipts.ts +3 -14
- package/lib/task/artifact-recovery.ts +519 -0
- package/lib/task/artifact-repair-intent.ts +162 -33
- package/lib/task/artifact-schema.ts +5 -34
- package/lib/task/capabilities.ts +13 -22
- package/lib/task/codex-orchestration.ts +85 -24
- package/lib/task/commit-identity.ts +31 -0
- package/lib/task/commit-operation.ts +37 -20
- package/lib/task/control-authority.ts +540 -10
- package/lib/task/control-recovery.ts +186 -0
- package/lib/task/create-service.ts +1 -10
- package/lib/task/create.ts +63 -8
- package/lib/task/decision-details.ts +3 -71
- package/lib/task/delegation-receipts.ts +73 -2
- package/lib/task/events.ts +309 -24
- package/lib/task/finalization-state.ts +11 -0
- package/lib/task/human-override.ts +8 -16
- package/lib/task/implementation-inputs.ts +3 -3
- package/lib/task/intermediate-cleanup.ts +493 -0
- package/lib/task/invalidation.ts +18 -62
- package/lib/task/ledger-intents.ts +7 -24
- package/lib/task/ledger.ts +1 -3
- package/lib/task/lifecycle-recovery.ts +691 -0
- package/lib/task/lifecycle.ts +30 -5
- package/lib/task/local-artifact-finalization.ts +244 -234
- package/lib/task/manual-validation-completion.ts +40 -0
- package/lib/task/manual-validation-evidence.ts +209 -0
- package/lib/task/manual-validation-receipt.ts +156 -0
- package/lib/task/manual-validation-shared.ts +50 -0
- package/lib/task/manual-validation-transaction.ts +304 -0
- package/lib/task/markdown.ts +70 -0
- package/lib/task/orchestration.ts +84 -19
- package/lib/task/pr-delivery-fact.ts +15 -62
- package/lib/task/qualification-audit.ts +22 -33
- package/lib/task/recovery-warning.ts +21 -0
- package/lib/task/review-artifacts.ts +1 -20
- package/lib/task/review-command.ts +24 -0
- package/lib/task/review-finalization.ts +183 -292
- package/lib/task/review-fingerprint.ts +2 -2
- package/lib/task/sections.ts +33 -54
- package/lib/task/short-id.ts +27 -5
- package/lib/task/task-execution-lock.ts +1 -1
- package/lib/task/verification-engine.ts +113 -383
- package/lib/task/verification-types.ts +15 -0
- package/lib/task/verification.ts +1 -1
- package/lib/task/workflow-command.ts +17 -0
- package/lib/task/workflow-warning-intents.ts +0 -13
- package/lib/task/workflow-warnings.ts +21 -2
- package/lib/update.ts +6 -0
- package/lib/version.ts +1 -15
- package/package.json +6 -6
- package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
- package/templates/.agents/rules/create-issue.en.md +1 -1
- package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +4 -2
- package/templates/.agents/rules/issue-sync.zh-CN.md +4 -3
- package/templates/.agents/rules/local-artifact-repair.en.md +46 -39
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +43 -36
- package/templates/.agents/rules/testing-discipline.en.md +2 -0
- package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
- package/templates/.agents/scripts/package.json +3 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/config/verify.json +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +7 -7
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +33 -21
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +33 -21
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
- package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/create-pr/config/verify.json +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/create-task/config/verify.json +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/import-issue/config/verify.json +2 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/release/SKILL.en.md +7 -3
- package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
- package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/restore-task/config/verify.json +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
- package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
- package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +20 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +20 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test/SKILL.en.md +17 -6
- package/templates/.agents/skills/test/SKILL.zh-CN.md +17 -6
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/import-issue.en.md +1 -1
- package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
- package/templates/.claude/commands/restore-task.en.md +1 -1
- package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
- package/templates/.claude/commands/run-manual-validation.en.md +1 -1
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
- package/dist/lib/platform/adapters.js +0 -56
- package/lib/platform/adapters.ts +0 -197
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { inspectOwnedPath } from '../owned-path.ts';
|
|
4
|
+
import { parseTaskFrontmatter } from '../task/frontmatter.ts';
|
|
5
|
+
import { readTaskFinalizationReceipt, type TaskFinalizationReceipt } from '../task/finalization.ts';
|
|
6
|
+
import { taskFinalizationReceiptState, type TaskControlBindingEvidence, type TaskControlBinding as SandboxControlBinding } from '../task/finalization-state.ts';
|
|
7
|
+
import { enumerateAllTaskDirs } from '../task/resolve-ref.ts';
|
|
8
|
+
import { resolveSandboxCleanupTarget } from './workspace-identity.ts';
|
|
9
|
+
import { readSandboxControlManifest } from './control/lifecycle.ts';
|
|
10
|
+
import { readJsonFile, readSandboxControlStatus } from './control/state.ts';
|
|
11
|
+
import { finalizationTerminalResponse } from './control/finalization-response.ts';
|
|
12
|
+
|
|
13
|
+
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
14
|
+
|
|
15
|
+
function matchingControlManifest(repoRoot: string, controlRoot: string, taskId: string, binding: SandboxControlBinding) {
|
|
16
|
+
if (!controlRootIdentity(controlRoot)) return null;
|
|
17
|
+
const manifestPath = path.join(controlRoot, 'manifest.json');
|
|
18
|
+
let manifest;
|
|
19
|
+
try { manifest = readSandboxControlManifest(manifestPath); }
|
|
20
|
+
catch { return null; }
|
|
21
|
+
if (manifest.mode !== 'task-bound' || manifest.taskId !== taskId
|
|
22
|
+
|| canonicalPath(manifest.repoRoot) !== repoRoot || manifest.generation !== binding.generation
|
|
23
|
+
|| path.resolve(manifest.channelDir) !== path.join(controlRoot, 'channel')
|
|
24
|
+
|| path.resolve(manifest.publicStatusDir) !== path.join(controlRoot, 'public')
|
|
25
|
+
|| path.resolve(manifest.processingDir) !== path.join(controlRoot, 'processing')
|
|
26
|
+
|| path.resolve(manifest.runtimeDir) !== path.join(controlRoot, 'runtime')) return null;
|
|
27
|
+
return manifest;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type DirectoryIdentity = Readonly<{ dev: string; ino: string }>;
|
|
31
|
+
type SandboxRemovalJournalEvidence = Readonly<{
|
|
32
|
+
phase: string;
|
|
33
|
+
generation: string;
|
|
34
|
+
target: Readonly<{
|
|
35
|
+
branch: string;
|
|
36
|
+
controlRoot: string;
|
|
37
|
+
}>;
|
|
38
|
+
}>;
|
|
39
|
+
|
|
40
|
+
function errorCode(error: unknown): string | null {
|
|
41
|
+
const code = (error as NodeJS.ErrnoException | null)?.code;
|
|
42
|
+
return typeof code === 'string' ? code : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function controlBindingKey(taskId: string, binding: SandboxControlBinding): string {
|
|
46
|
+
return `${taskId}\0${binding.generation}\0${binding.requestId}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function controlRootIdentity(root: string): DirectoryIdentity | null {
|
|
50
|
+
try {
|
|
51
|
+
const stat = fs.lstatSync(root);
|
|
52
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
|
53
|
+
return { dev: String(stat.dev), ino: String(stat.ino) };
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function controlRootState(
|
|
60
|
+
root: string,
|
|
61
|
+
expected: DirectoryIdentity
|
|
62
|
+
): 'missing' | 'same' | 'replaced' {
|
|
63
|
+
let stat: fs.Stats;
|
|
64
|
+
try { stat = fs.lstatSync(root); }
|
|
65
|
+
catch (error) { return errorCode(error) === 'ENOENT' ? 'missing' : 'replaced'; }
|
|
66
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return 'replaced';
|
|
67
|
+
return String(stat.dev) === expected.dev && String(stat.ino) === expected.ino ? 'same' : 'replaced';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function controlRootIsAbsent(root: string): boolean {
|
|
71
|
+
try {
|
|
72
|
+
fs.lstatSync(root);
|
|
73
|
+
return false;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return errorCode(error) === 'ENOENT';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function canonicalPath(input: string): string {
|
|
80
|
+
try { return fs.realpathSync.native(input); }
|
|
81
|
+
catch { return path.resolve(input); }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function taskFinalizationReceiptComplete(
|
|
85
|
+
receipt: TaskFinalizationReceipt,
|
|
86
|
+
taskId: string,
|
|
87
|
+
binding: SandboxControlBinding
|
|
88
|
+
): boolean {
|
|
89
|
+
return receipt.taskId === taskId
|
|
90
|
+
&& taskFinalizationReceiptState(receipt) === 'complete'
|
|
91
|
+
&& receipt.controlBinding?.generation === binding.generation
|
|
92
|
+
&& receipt.controlBinding.requestId === binding.requestId;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function terminalControlBindingEvidence(
|
|
96
|
+
repoRootInput: string,
|
|
97
|
+
controlRootInput: string,
|
|
98
|
+
taskId: string,
|
|
99
|
+
binding: SandboxControlBinding
|
|
100
|
+
): boolean {
|
|
101
|
+
const repoRoot = canonicalPath(repoRootInput);
|
|
102
|
+
const controlRoot = path.resolve(controlRootInput);
|
|
103
|
+
if (!TASK_ID_RE.test(taskId) || !controlRootIdentity(controlRoot)) return false;
|
|
104
|
+
const manifestPath = path.join(controlRoot, 'manifest.json');
|
|
105
|
+
if (!inspectOwnedPath(controlRoot, manifestPath, 'file')) return false;
|
|
106
|
+
const manifest = matchingControlManifest(repoRoot, controlRoot, taskId, binding);
|
|
107
|
+
if (!manifest) return false;
|
|
108
|
+
try {
|
|
109
|
+
const leasePath = path.join(controlRoot, 'lease.json');
|
|
110
|
+
try {
|
|
111
|
+
fs.lstatSync(leasePath);
|
|
112
|
+
return false;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (errorCode(error) !== 'ENOENT') return false;
|
|
115
|
+
}
|
|
116
|
+
const receipt = readTaskFinalizationReceipt(repoRoot, taskId);
|
|
117
|
+
if (!receipt || !taskFinalizationReceiptComplete(receipt, taskId, binding)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
const status = readSandboxControlStatus(manifest.publicStatusDir);
|
|
121
|
+
const viewReceipt = status.taskView.receipt;
|
|
122
|
+
const terminalTaskView = (status.taskView.state === 'current' && status.taskView.observedSource === 'completed')
|
|
123
|
+
|| (status.taskView.state === 'finalized-stale' && status.taskView.observedSource === 'active');
|
|
124
|
+
if (status.generation !== binding.generation || status.state !== 'healthy'
|
|
125
|
+
|| status.activeRequestId !== null || !terminalTaskView || status.taskView.taskId !== taskId
|
|
126
|
+
|| !viewReceipt || viewReceipt.generation !== binding.generation
|
|
127
|
+
|| viewReceipt.requestId !== binding.requestId
|
|
128
|
+
|| viewReceipt.receiptId !== receipt.receiptId
|
|
129
|
+
|| viewReceipt.revision !== receipt.revision) return false;
|
|
130
|
+
const responsePath = path.join(manifest.channelDir, 'responses', `${binding.requestId}.json`);
|
|
131
|
+
if (!inspectOwnedPath(path.join(controlRoot, 'channel'), responsePath, 'file')) return false;
|
|
132
|
+
const actual = readJsonFile(responsePath);
|
|
133
|
+
const expected = finalizationTerminalResponse(taskId, binding.requestId, receipt);
|
|
134
|
+
return JSON.stringify(actual) === JSON.stringify(expected);
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function removedControlBindingEvidence(
|
|
141
|
+
repoRootInput: string,
|
|
142
|
+
journal: SandboxRemovalJournalEvidence,
|
|
143
|
+
taskId: string,
|
|
144
|
+
binding: SandboxControlBinding
|
|
145
|
+
): boolean {
|
|
146
|
+
const repoRoot = canonicalPath(repoRootInput);
|
|
147
|
+
if (journal.phase !== 'completed'
|
|
148
|
+
|| journal.generation !== binding.generation
|
|
149
|
+
|| !path.isAbsolute(journal.target.controlRoot)) return false;
|
|
150
|
+
const controlRoot = path.resolve(journal.target.controlRoot);
|
|
151
|
+
if (!controlRootIsAbsent(controlRoot)) return false;
|
|
152
|
+
const task = enumerateAllTaskDirs(repoRoot).find((entry) => entry.taskId === taskId);
|
|
153
|
+
if (!task || (task.state !== 'completed' && task.state !== 'archive')) return false;
|
|
154
|
+
let frontmatter: Record<string, string>;
|
|
155
|
+
try { frontmatter = parseTaskFrontmatter(fs.readFileSync(path.join(task.taskDir, 'task.md'), 'utf8')); }
|
|
156
|
+
catch { return false; }
|
|
157
|
+
if (frontmatter.id !== taskId || frontmatter.branch !== journal.target.branch) return false;
|
|
158
|
+
try {
|
|
159
|
+
const receipt = readTaskFinalizationReceipt(repoRoot, taskId);
|
|
160
|
+
return receipt !== null && taskFinalizationReceiptComplete(receipt, taskId, binding);
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function createSandboxControlBindingEvidence(
|
|
167
|
+
repoRoot: string,
|
|
168
|
+
controlRoots: readonly string[],
|
|
169
|
+
removalJournals: readonly SandboxRemovalJournalEvidence[] = []
|
|
170
|
+
): TaskControlBindingEvidence {
|
|
171
|
+
const canonicalRepoRoot = canonicalPath(repoRoot);
|
|
172
|
+
const roots = [...new Set(controlRoots.map((candidate) => path.resolve(candidate)))];
|
|
173
|
+
const captured = roots.flatMap((controlRoot) => {
|
|
174
|
+
let manifest;
|
|
175
|
+
try { manifest = readSandboxControlManifest(path.join(controlRoot, 'manifest.json')); }
|
|
176
|
+
catch { return []; }
|
|
177
|
+
const rootIdentity = controlRootIdentity(controlRoot);
|
|
178
|
+
if (!rootIdentity) return [];
|
|
179
|
+
const receipt = (() => {
|
|
180
|
+
try { return readSandboxControlStatus(manifest.publicStatusDir).taskView.receipt; }
|
|
181
|
+
catch { return null; }
|
|
182
|
+
})();
|
|
183
|
+
if (!receipt || !terminalControlBindingEvidence(canonicalRepoRoot, controlRoot, manifest.taskId ?? '', receipt)) return [];
|
|
184
|
+
return [{
|
|
185
|
+
controlRoot,
|
|
186
|
+
taskId: manifest.taskId!,
|
|
187
|
+
key: controlBindingKey(manifest.taskId!, receipt),
|
|
188
|
+
rootIdentity
|
|
189
|
+
}];
|
|
190
|
+
});
|
|
191
|
+
return (taskId, binding) => {
|
|
192
|
+
const terminal = captured.some((candidate) => {
|
|
193
|
+
if (candidate.taskId !== taskId || candidate.key !== controlBindingKey(taskId, binding)) return false;
|
|
194
|
+
const state = controlRootState(candidate.controlRoot, candidate.rootIdentity);
|
|
195
|
+
return state === 'missing'
|
|
196
|
+
|| state === 'same' && terminalControlBindingEvidence(canonicalRepoRoot, candidate.controlRoot, taskId, binding);
|
|
197
|
+
}) || removalJournals.some((journal) => removedControlBindingEvidence(canonicalRepoRoot, journal, taskId, binding));
|
|
198
|
+
if (terminal) return 'terminal';
|
|
199
|
+
if (roots.some((root) => matchingControlManifest(canonicalRepoRoot, root, taskId, binding))) return 'pending';
|
|
200
|
+
try {
|
|
201
|
+
const branch = resolveSandboxCleanupTarget(taskId, canonicalRepoRoot, { allowProtected: true }).branch;
|
|
202
|
+
if (removalJournals.some((journal) => journal.phase !== 'completed'
|
|
203
|
+
&& journal.generation === binding.generation && journal.target.branch === branch)) return 'pending';
|
|
204
|
+
} catch {
|
|
205
|
+
// Missing or conflicting task identity cannot authorize removal.
|
|
206
|
+
}
|
|
207
|
+
return 'mismatch';
|
|
208
|
+
};
|
|
209
|
+
}
|
|
@@ -23,7 +23,7 @@ export type SandboxCleanupTarget = Readonly<{
|
|
|
23
23
|
requestedRef: string;
|
|
24
24
|
branch: string;
|
|
25
25
|
workspace: SandboxWorkspaceKey;
|
|
26
|
-
taskState: TaskWorkspaceState | 'branch-only';
|
|
26
|
+
taskState: TaskWorkspaceState | 'branch-only' | 'unknown';
|
|
27
27
|
}>;
|
|
28
28
|
|
|
29
29
|
export type SandboxContainerWorkspaceIdentity =
|
|
@@ -147,14 +147,30 @@ export function resolveSandboxReentryContext(params: Readonly<{
|
|
|
147
147
|
export function resolveSandboxCleanupTarget(
|
|
148
148
|
requestedRef: string,
|
|
149
149
|
repoRoot: string,
|
|
150
|
-
options: Readonly<{
|
|
150
|
+
options: Readonly<{
|
|
151
|
+
allowProtected?: boolean;
|
|
152
|
+
resolveMissingTask?: (taskId: string) => string | null;
|
|
153
|
+
}> = {}
|
|
151
154
|
): SandboxCleanupTarget {
|
|
152
155
|
if (isRemovedHashShortIdInput(requestedRef)) {
|
|
153
156
|
throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
if (TASK_ID_RE.test(requestedRef)) {
|
|
157
|
-
|
|
160
|
+
let task: ReturnType<typeof resolveTaskWorkspace>;
|
|
161
|
+
try {
|
|
162
|
+
task = resolveTaskWorkspace(requestedRef, repoRoot);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (!(error instanceof Error && error.message === `Task not found: ${requestedRef}`)) throw error;
|
|
165
|
+
const branch = options.resolveMissingTask?.(requestedRef);
|
|
166
|
+
if (!branch) throw error;
|
|
167
|
+
return {
|
|
168
|
+
requestedRef,
|
|
169
|
+
branch,
|
|
170
|
+
workspace: { mode: 'task-bound', taskId: requestedRef },
|
|
171
|
+
taskState: 'unknown'
|
|
172
|
+
};
|
|
173
|
+
}
|
|
158
174
|
if (!options.allowProtected && (task.state === 'blocked' || task.state === 'archive')) {
|
|
159
175
|
throw new Error(`SANDBOX_CLEANUP_STATE_UNSUPPORTED: task ${requestedRef} is ${task.state}`);
|
|
160
176
|
}
|
|
@@ -3,10 +3,13 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import type { SandboxWorkspaceIdentity, SandboxWorkspaceKey } from './workspace-identity.ts';
|
|
5
5
|
import type { SandboxControlManifest } from './control/protocol.ts';
|
|
6
|
+
import {
|
|
7
|
+
createSandboxControlIdentitySentinel,
|
|
8
|
+
writeSandboxControlIdentitySentinel
|
|
9
|
+
} from './control/identity-sentinel.ts';
|
|
6
10
|
|
|
7
11
|
export type SandboxWorkspaceView = Readonly<{
|
|
8
12
|
root: string;
|
|
9
|
-
taskMountPath: string | null;
|
|
10
13
|
}>;
|
|
11
14
|
|
|
12
15
|
export const SANDBOX_WORKSPACE_VIEW_STATES = Object.freeze([
|
|
@@ -38,12 +41,7 @@ export function sandboxWorkspaceViewPaths(params: Readonly<{
|
|
|
38
41
|
const digest = createHash('sha256').update(identityKey).digest('hex').slice(0, 16);
|
|
39
42
|
const projectRoot = path.resolve(params.base, params.project);
|
|
40
43
|
const root = path.resolve(projectRoot, params.container, digest);
|
|
41
|
-
return {
|
|
42
|
-
root,
|
|
43
|
-
taskMountPath: params.identity.mode === 'task-bound'
|
|
44
|
-
? path.join(root, 'active', params.identity.taskId)
|
|
45
|
-
: null
|
|
46
|
-
};
|
|
44
|
+
return { root };
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
export type SandboxControlSetup = Readonly<{
|
|
@@ -55,6 +53,7 @@ export type SandboxControlSetup = Readonly<{
|
|
|
55
53
|
manifestDraft: SandboxControlManifestDraft;
|
|
56
54
|
token: string;
|
|
57
55
|
generation: string;
|
|
56
|
+
controlRootId: string;
|
|
58
57
|
}>;
|
|
59
58
|
|
|
60
59
|
export type SandboxControlManifestDraft = Readonly<Omit<SandboxControlManifest, 'containerIdentity' | 'engine' | 'authorityEvidence'> & {
|
|
@@ -158,7 +157,7 @@ export function materializeSandboxWorkspaceView(params: Readonly<{
|
|
|
158
157
|
identity: SandboxWorkspaceIdentity;
|
|
159
158
|
}>): SandboxWorkspaceView {
|
|
160
159
|
const projectRoot = path.resolve(params.base, params.project);
|
|
161
|
-
const { root
|
|
160
|
+
const { root } = sandboxWorkspaceViewPaths(params);
|
|
162
161
|
assertSafeDirectory(projectRoot, path.resolve(params.base));
|
|
163
162
|
assertSafeDirectory(root, projectRoot);
|
|
164
163
|
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
@@ -179,11 +178,7 @@ export function materializeSandboxWorkspaceView(params: Readonly<{
|
|
|
179
178
|
encoding: 'utf8',
|
|
180
179
|
mode: 0o600
|
|
181
180
|
});
|
|
182
|
-
|
|
183
|
-
assertSafeDirectory(taskMountPath, active);
|
|
184
|
-
fs.mkdirSync(taskMountPath, { recursive: true, mode: 0o700 });
|
|
185
|
-
}
|
|
186
|
-
return { root, taskMountPath };
|
|
181
|
+
return { root };
|
|
187
182
|
}
|
|
188
183
|
|
|
189
184
|
export function materializeSandboxControl(params: Readonly<{
|
|
@@ -234,6 +229,12 @@ export function materializeSandboxControl(params: Readonly<{
|
|
|
234
229
|
}
|
|
235
230
|
const token = randomBytes(32).toString('hex');
|
|
236
231
|
const generation = randomBytes(16).toString('hex');
|
|
232
|
+
const identitySentinel = createSandboxControlIdentitySentinel({
|
|
233
|
+
mode: params.identity.mode,
|
|
234
|
+
taskId: params.identity.mode === 'task-bound' ? params.identity.taskId : null,
|
|
235
|
+
generation
|
|
236
|
+
});
|
|
237
|
+
writeSandboxControlIdentitySentinel(statusDir, identitySentinel);
|
|
237
238
|
const repoRoot = fs.realpathSync.native(params.repoRoot);
|
|
238
239
|
const manifestDraft: SandboxControlManifestDraft = {
|
|
239
240
|
engine: params.engine ?? 'docker',
|
|
@@ -246,12 +247,13 @@ export function materializeSandboxControl(params: Readonly<{
|
|
|
246
247
|
taskId: params.identity.mode === 'task-bound' ? params.identity.taskId : null,
|
|
247
248
|
token,
|
|
248
249
|
generation,
|
|
250
|
+
controlRootId: identitySentinel.controlRootId,
|
|
249
251
|
channelDir,
|
|
250
252
|
publicStatusDir: statusDir,
|
|
251
253
|
processingDir,
|
|
252
|
-
runtimeDir
|
|
254
|
+
runtimeDir,
|
|
253
255
|
};
|
|
254
|
-
return { root, channelDir, statusDir, runtimeDir, manifestPath, manifestDraft, token, generation };
|
|
256
|
+
return { root, channelDir, statusDir, runtimeDir, manifestPath, manifestDraft, token, generation, controlRootId: identitySentinel.controlRootId };
|
|
255
257
|
}
|
|
256
258
|
|
|
257
259
|
export function finalizeSandboxControlManifest(
|
|
@@ -6,9 +6,9 @@ import { locateActivityLog, appendActivityEntry } from './activity-log.ts';
|
|
|
6
6
|
import {
|
|
7
7
|
assertWritableInventory,
|
|
8
8
|
buildArtifactLinkSection,
|
|
9
|
-
inspectTaskArtifacts
|
|
10
|
-
parseArtifactName
|
|
9
|
+
inspectTaskArtifacts
|
|
11
10
|
} from './artifact-lifecycle.ts';
|
|
11
|
+
import { parseArtifactName } from './artifact-name.ts';
|
|
12
12
|
import type { ArtifactIdentity } from './artifact-lifecycle.ts';
|
|
13
13
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
14
14
|
import type { TaskExecutionLockOptions } from './task-execution-lock.ts';
|
package/lib/task/activity-log.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
const NEXT_H2_RE = /^##\s/;
|
|
1
|
+
import { scanVisibleMarkdown } from './markdown.ts';
|
|
3
2
|
const ENTRY_RE = /^- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}) — \*\*(.+?)\*\* by (.+?) — (.*)$/;
|
|
4
3
|
const STARTED_SUFFIX_RE = /\s*\[started\]\s*$/;
|
|
5
4
|
const ABORTED_SUFFIX_RE = /\s*\[aborted\]\s*$/;
|
|
@@ -17,24 +16,32 @@ function parseActivityLog(content: string): { sectionFound: boolean; entries: Lo
|
|
|
17
16
|
return section ? { sectionFound: true, entries: section.entries } : { sectionFound: false, entries: [] };
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
/** Returns source-order entries and malformed bullets without applying display or gate policy. */
|
|
20
|
+
function inspectActivityLog(content: string): { section: ActivityLogSection | null; invalidEntries: string[] } {
|
|
21
|
+
const scanned = scanVisibleMarkdown(content);
|
|
22
|
+
const headings = scanned.headings.filter((heading) => heading.level === 2);
|
|
23
|
+
const matches = headings.filter((heading) => heading.text === '活动日志' || heading.text === 'Activity Log');
|
|
24
|
+
if (matches.length !== 1) return { section: null, invalidEntries: [] };
|
|
25
|
+
const heading = matches[0]!;
|
|
26
|
+
const end = headings.find((candidate) => candidate.start > heading.start)?.start ?? content.length;
|
|
27
|
+
const entries: LogEntry[] = [];
|
|
28
|
+
const invalidEntries: string[] = [];
|
|
29
|
+
for (const line of scanned.lines) {
|
|
30
|
+
if (line.start <= heading.end || line.start >= end) continue;
|
|
31
|
+
const text = line.text.trimStart();
|
|
32
|
+
const match = ENTRY_RE.exec(text);
|
|
33
|
+
if (match) entries.push({ time: match[1]!, step: match[2]!, agent: match[3]!, note: match[4]! });
|
|
34
|
+
else if (text.startsWith('- ')) invalidEntries.push(text);
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
return {
|
|
36
|
+
const body = content.slice(heading.end + 1, end).replace(/\r\n/g, '\n').replace(/^\n+|\n+$/g, '');
|
|
37
|
+
return { section: { heading: heading.text, body, entries }, invalidEntries };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function locateActivityLog(content: string): ActivityLogSection | null {
|
|
41
|
+
const { section } = inspectActivityLog(content);
|
|
42
|
+
if (!section) return null;
|
|
43
|
+
section.entries.sort((left, right) => Date.parse(left.time.replace(' ', 'T')) - Date.parse(right.time.replace(' ', 'T')));
|
|
44
|
+
return section;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
function appendActivityEntry(section: ActivityLogSection, entry: LogEntry): string {
|
|
@@ -105,6 +112,7 @@ export {
|
|
|
105
112
|
appendActivityEntry,
|
|
106
113
|
hasOpenLifecycleExecution,
|
|
107
114
|
commitAttemptStartedNote,
|
|
115
|
+
inspectActivityLog,
|
|
108
116
|
locateActivityLog,
|
|
109
117
|
pairEntries,
|
|
110
118
|
parseActivityLog,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import { resolveArtifactContext, hasOpenArtifactRound } from './artifact-lifecycle.ts';
|
|
4
|
+
import { parseArtifactName } from './artifact-name.ts';
|
|
5
|
+
import { resolveTaskRef } from './resolve-ref.ts';
|
|
6
|
+
import { getArtifactSchema } from './artifact-schema.ts';
|
|
7
|
+
import { finalizeLocalArtifact } from './local-artifact-finalization.ts';
|
|
8
|
+
import { initializeArtifactSkeleton } from './artifact-operations.ts';
|
|
9
|
+
|
|
10
|
+
export type ArtifactCommand = Readonly<{
|
|
11
|
+
taskRef: string;
|
|
12
|
+
operation: 'inspect' | 'init' | 'finalize-local';
|
|
13
|
+
family: string;
|
|
14
|
+
artifact: string;
|
|
15
|
+
locale?: 'zh-CN' | 'en';
|
|
16
|
+
recoveryId?: string;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
/** One option contract for the CLI and trusted projection executor. */
|
|
20
|
+
export function parseArtifactCommand(args: readonly string[]): ArtifactCommand {
|
|
21
|
+
const [taskRef, operation] = args;
|
|
22
|
+
if (!taskRef || !['inspect', 'init', 'finalize-local'].includes(operation ?? '')) {
|
|
23
|
+
throw new Error('task ref and a supported artifact operation are required');
|
|
24
|
+
}
|
|
25
|
+
const fields: Record<string, string> = {};
|
|
26
|
+
const allowed = {
|
|
27
|
+
inspect: ['--family'],
|
|
28
|
+
init: ['--family', '--artifact', '--locale'],
|
|
29
|
+
'finalize-local': ['--family', '--artifact', '--recovery-id']
|
|
30
|
+
}[operation as ArtifactCommand['operation']];
|
|
31
|
+
for (let index = 2; index < args.length; index += 1) {
|
|
32
|
+
const flag = args[index]!;
|
|
33
|
+
if (!allowed.includes(flag)) throw new Error(`unknown option '${flag}'`);
|
|
34
|
+
if (Object.hasOwn(fields, flag)) throw new Error(`duplicate option '${flag}'`);
|
|
35
|
+
const value = args[++index];
|
|
36
|
+
if (!value || value.startsWith('--')) throw new Error(`option '${flag}' requires a value`);
|
|
37
|
+
fields[flag] = value;
|
|
38
|
+
}
|
|
39
|
+
if (!fields['--family']) throw new Error("option '--family' is required");
|
|
40
|
+
if (operation !== 'inspect' && !fields['--artifact']) throw new Error("option '--artifact' is required");
|
|
41
|
+
const locale = fields['--locale'];
|
|
42
|
+
if (locale !== undefined && locale !== 'zh-CN' && locale !== 'en') throw new Error("option '--locale' must be 'zh-CN' or 'en'");
|
|
43
|
+
return {
|
|
44
|
+
taskRef, operation: operation as ArtifactCommand['operation'], family: fields['--family'],
|
|
45
|
+
artifact: fields['--artifact'] ?? '', locale, recoveryId: fields['--recovery-id']
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Resolve authoritative task metadata separately from a trusted candidate directory. */
|
|
49
|
+
export function executeArtifactCommand(
|
|
50
|
+
command: ArtifactCommand,
|
|
51
|
+
options: Readonly<{ repoRoot?: string; artifactDir?: string }> = {}
|
|
52
|
+
): Record<string, unknown> {
|
|
53
|
+
const { taskRef, operation, family, artifact, locale, recoveryId } = command;
|
|
54
|
+
const fail = (code: string, message: string) => ({ status: 'failed', changed: false, error: { code, message } });
|
|
55
|
+
if (operation === 'inspect') {
|
|
56
|
+
const result = resolveArtifactContext(taskRef, family, { repoRoot: options.repoRoot });
|
|
57
|
+
return family === 'code' && result.codeMode ? {
|
|
58
|
+
...result, mode: result.codeMode.mode, code_max: result.codeMode.codeMax, rev_max: result.codeMode.reviewMax,
|
|
59
|
+
verdict: result.codeMode.verdict, next_round: result.next?.round ?? null, next_artifact: result.next?.name ?? null,
|
|
60
|
+
review_artifact: result.codeMode.reviewArtifact, implementation_input: result.codeMode.implementationInput,
|
|
61
|
+
decision_id: result.codeMode.decisionId, decision_evidence: result.codeMode.decisionEvidence, message: result.codeMode.message
|
|
62
|
+
} : { ...result };
|
|
63
|
+
}
|
|
64
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
65
|
+
if (!resolved.ok) return fail(resolved.code, resolved.message);
|
|
66
|
+
const schema = getArtifactSchema(family);
|
|
67
|
+
const parsed = parseArtifactName(artifact);
|
|
68
|
+
if (!schema || !parsed || parsed.family !== family) {
|
|
69
|
+
return fail('ARTIFACT_IDENTITY_INVALID', `artifact '${artifact}' does not match ${family}`);
|
|
70
|
+
}
|
|
71
|
+
const taskDir = options.artifactDir ?? resolved.taskDir;
|
|
72
|
+
const identity = { taskId: resolved.taskId, taskDir, family, artifact };
|
|
73
|
+
const input = { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskDir, family: schema.family, artifact };
|
|
74
|
+
if (operation === 'init') {
|
|
75
|
+
const context = resolveArtifactContext(taskRef, family, { repoRoot: resolved.repoRoot });
|
|
76
|
+
if (context.status !== 'ready' || (context.next?.name !== artifact && !hasOpenArtifactRound(fs.readFileSync(resolved.taskMdPath, 'utf8'), family, parsed.round))) {
|
|
77
|
+
return fail('ARTIFACT_INIT_CONTEXT_INVALID', context.error?.message ?? `artifact '${artifact}' is not the next ${family} artifact`);
|
|
78
|
+
}
|
|
79
|
+
return { ...initializeArtifactSkeleton({ ...input, ...(locale ? { locale } : {}) }), ...identity };
|
|
80
|
+
}
|
|
81
|
+
if (family !== 'analysis' && family !== 'plan' && family !== 'code') {
|
|
82
|
+
return fail('ARTIFACT_PAYLOAD_INVALID', "finalize-local only supports 'analysis', 'plan', and 'code'");
|
|
83
|
+
}
|
|
84
|
+
return { ...finalizeLocalArtifact({ taskRef, family, artifact, repoRoot: resolved.repoRoot, recoveryId }) };
|
|
85
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
import { ARTIFACT_FAMILY_CATALOG, artifactName, parseArtifactName } from './artifact-name.ts';
|
|
4
5
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
5
6
|
import type { ResolveTaskRefErrorCode, TaskWorkspaceState } from './resolve-ref.ts';
|
|
6
|
-
import { locateActivityLog } from './activity-log.ts';
|
|
7
|
+
import { locateActivityLog, pairEntries, startedBackedRows } from './activity-log.ts';
|
|
7
8
|
import { parseImplementationInputs, selectPendingImplementationInput } from './implementation-inputs.ts';
|
|
8
9
|
import { parseVerdict } from './review-artifacts.ts';
|
|
9
10
|
import { extractSection, findSectionHeading } from './sections.ts';
|
|
@@ -11,10 +12,27 @@ import { receiptForOutput, sha256File } from './artifact-receipts.ts';
|
|
|
11
12
|
import { isArtifactInvalidated, parseInvalidationDocument } from './invalidation.ts';
|
|
12
13
|
import type { InvalidationDocument } from './invalidation.ts';
|
|
13
14
|
import { validateQualificationAudit } from './qualification-audit.ts';
|
|
14
|
-
import {
|
|
15
|
-
import type { ArtifactFamily, ArtifactFamilySpec } from './artifact-schema.ts';
|
|
15
|
+
import type { ArtifactFamily, ArtifactFamilySpec } from './artifact-name.ts';
|
|
16
16
|
|
|
17
17
|
const artifactFamilyCatalog = ARTIFACT_FAMILY_CATALOG;
|
|
18
|
+
const ARTIFACT_STEPS: Readonly<Record<string, string>> = {
|
|
19
|
+
analysis: 'Analyze Task', 'review-analysis': 'Review Analysis',
|
|
20
|
+
plan: 'Plan Task', 'review-plan': 'Review Plan',
|
|
21
|
+
code: 'Code Task', 'review-code': 'Review Code'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Init, repair and finalization share the same authoritative round identity. */
|
|
25
|
+
export function hasOpenArtifactRound(content: string, family: string, round: number): boolean {
|
|
26
|
+
const activity = locateActivityLog(content);
|
|
27
|
+
const step = ARTIFACT_STEPS[family];
|
|
28
|
+
if (!activity || !step) return false;
|
|
29
|
+
const qualifier = family === 'code'
|
|
30
|
+
? '(?:, (?:fix for review-code(?:-r(?:[2-9]|[1-9]\\d+))?\\.md|decision II-[1-9]\\d*))?'
|
|
31
|
+
: '';
|
|
32
|
+
const expected = new RegExp(`^${step} \\(Round ${round}${qualifier}\\)$`);
|
|
33
|
+
return startedBackedRows(pairEntries(activity.entries)).filter((row) => expected.test(row.step) && !row.done).length === 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
18
36
|
type ArtifactIdentity = {
|
|
19
37
|
family: ArtifactFamily;
|
|
20
38
|
round: number;
|
|
@@ -75,7 +93,6 @@ type ArtifactContextResult = Omit<ArtifactInventoryResult, 'status'> & {
|
|
|
75
93
|
};
|
|
76
94
|
type InspectOptions = { repoRoot?: string };
|
|
77
95
|
|
|
78
|
-
const FAMILY_SET = new Set<string>(artifactFamilyCatalog.map((item) => item.family));
|
|
79
96
|
const BLOCKING_DIAGNOSTICS = new Set<ArtifactDiagnosticCode>([
|
|
80
97
|
'NONCANONICAL_NAME', 'ROUND_OUT_OF_RANGE', 'MISSING_BASE', 'ROUND_GAP',
|
|
81
98
|
'DUPLICATE_LOGICAL_ROUND', 'NON_REGULAR_FILE', 'SYMBOLIC_LINK',
|
|
@@ -86,27 +103,6 @@ function familySpec(family: string): ArtifactFamilySpec | null {
|
|
|
86
103
|
return artifactFamilyCatalog.find((item) => item.family === family) ?? null;
|
|
87
104
|
}
|
|
88
105
|
|
|
89
|
-
function artifactName(family: ArtifactFamily, round: number): string {
|
|
90
|
-
if (!FAMILY_SET.has(family)) throw new Error(`unknown artifact family '${family}'`);
|
|
91
|
-
if (!Number.isSafeInteger(round) || round < 1) throw new Error('artifact round must be a safe positive integer');
|
|
92
|
-
return round === 1 ? `${family}.md` : `${family}-r${round}.md`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function parseArtifactName(name: string): { family: ArtifactFamily; round: number; name: string } | null {
|
|
96
|
-
if (path.basename(name) !== name) return null;
|
|
97
|
-
const matches: Array<{ family: ArtifactFamily; round: number; name: string }> = [];
|
|
98
|
-
for (const spec of artifactFamilyCatalog) {
|
|
99
|
-
if (name === `${spec.family}.md`) matches.push({ family: spec.family, round: 1, name });
|
|
100
|
-
const match = new RegExp(`^${escapeRegExp(spec.family)}-r([1-9]\\d*)\\.md$`).exec(name);
|
|
101
|
-
if (!match) continue;
|
|
102
|
-
const round = Number(match[1]);
|
|
103
|
-
if (Number.isSafeInteger(round) && round >= 2 && String(round) === match[1]) {
|
|
104
|
-
matches.push({ family: spec.family, round, name });
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return matches.length === 1 ? matches[0]! : null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
106
|
function escapeRegExp(value: string): string {
|
|
111
107
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
112
108
|
}
|
|
@@ -518,6 +514,15 @@ function validateCompletedArtifact(taskDir: string, family: ArtifactFamily, name
|
|
|
518
514
|
return artifact ? { ok: true, artifact } : { ok: false, error: { code: 'ARTIFACT_NOT_FOUND', message: `artifact '${name}' is not in inventory` } };
|
|
519
515
|
}
|
|
520
516
|
|
|
517
|
+
/** A candidate may replace its current slot or fill the next slot, never skip rounds. */
|
|
518
|
+
export function validateArtifactPublication(taskDir: string, family: ArtifactFamily, name: string): ArtifactError | null {
|
|
519
|
+
const inventory = inspectArtifactDirectory(taskDir, family);
|
|
520
|
+
const error = assertWritableInventory(inventory);
|
|
521
|
+
if (error) return error;
|
|
522
|
+
if (inventory.next?.name === name || inventory.artifacts.some((artifact) => artifact.name === name)) return null;
|
|
523
|
+
return { code: 'ARTIFACT_IDENTITY_INVALID', message: `artifact '${name}' is not a current or next ${family} slot` };
|
|
524
|
+
}
|
|
525
|
+
|
|
521
526
|
function buildArtifactLinkSection(content: string, artifact: ArtifactIdentity): {
|
|
522
527
|
aliases: readonly string[];
|
|
523
528
|
heading: string;
|
|
@@ -550,7 +555,7 @@ function buildArtifactLinkSection(content: string, artifact: ArtifactIdentity):
|
|
|
550
555
|
}
|
|
551
556
|
|
|
552
557
|
export {
|
|
553
|
-
artifactFamilyCatalog,
|
|
558
|
+
artifactFamilyCatalog, familySpec,
|
|
554
559
|
inspectTaskArtifacts, inspectArtifactDirectory, resolveArtifactContext,
|
|
555
560
|
parseReviewedInputReference, parseCodePlanInputReference, resolveCodePlanInput, reviewEventName,
|
|
556
561
|
assertWritableInventory, validateCompletedArtifact, buildArtifactLinkSection
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
type ArtifactFamily =
|
|
2
|
+
| 'analysis'
|
|
3
|
+
| 'review-analysis'
|
|
4
|
+
| 'plan'
|
|
5
|
+
| 'review-plan'
|
|
6
|
+
| 'code'
|
|
7
|
+
| 'review-code'
|
|
8
|
+
| 'manual-validation'
|
|
9
|
+
| 'validation-run'
|
|
10
|
+
| 'pr-review';
|
|
11
|
+
|
|
12
|
+
type ArtifactFamilySpec = Readonly<{
|
|
13
|
+
family: ArtifactFamily;
|
|
14
|
+
sectionAliases: readonly [string, string];
|
|
15
|
+
heading: string;
|
|
16
|
+
labels: readonly [string, string];
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
const ARTIFACT_FAMILY_CATALOG = [
|
|
20
|
+
{ family: 'analysis', sectionAliases: ['分析', 'Analysis'], heading: '分析', labels: ['需求分析报告', 'Requirements Analysis'] },
|
|
21
|
+
{ family: 'review-analysis', sectionAliases: ['审查反馈', 'Review Feedback'], heading: '审查反馈', labels: ['需求分析审查', 'Analysis Review'] },
|
|
22
|
+
{ family: 'plan', sectionAliases: ['设计', 'Design'], heading: '设计', labels: ['技术方案', 'Technical Plan'] },
|
|
23
|
+
{ family: 'review-plan', sectionAliases: ['审查反馈', 'Review Feedback'], heading: '审查反馈', labels: ['技术方案审查', 'Plan Review'] },
|
|
24
|
+
{ family: 'code', sectionAliases: ['实现备注', 'Implementation Notes'], heading: '实现备注', labels: ['实现报告', 'Implementation Report'] },
|
|
25
|
+
{ family: 'review-code', sectionAliases: ['审查反馈', 'Review Feedback'], heading: '审查反馈', labels: ['代码审查', 'Code Review'] },
|
|
26
|
+
{ family: 'manual-validation', sectionAliases: ['实现备注', 'Implementation Notes'], heading: '实现备注', labels: ['人工验证', 'Manual Validation'] },
|
|
27
|
+
{ family: 'validation-run', sectionAliases: ['实现备注', 'Implementation Notes'], heading: '实现备注', labels: ['验证运行证据', 'Validation Run Evidence'] },
|
|
28
|
+
{ family: 'pr-review', sectionAliases: ['审查反馈', 'Review Feedback'], heading: '审查反馈', labels: ['PR 审查报告', 'PR Review Report'] }
|
|
29
|
+
] as const satisfies readonly ArtifactFamilySpec[];
|
|
30
|
+
|
|
31
|
+
const FAMILIES = new Set<string>(ARTIFACT_FAMILY_CATALOG.map(({ family }) => family));
|
|
32
|
+
|
|
33
|
+
export function artifactName(family: ArtifactFamily, round: number): string {
|
|
34
|
+
if (!FAMILIES.has(family)) throw new Error(`unknown artifact family '${family}'`);
|
|
35
|
+
if (!Number.isSafeInteger(round) || round < 1) throw new Error('artifact round must be a safe positive integer');
|
|
36
|
+
return round === 1 ? `${family}.md` : `${family}-r${round}.md`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseArtifactName(name: string): { family: ArtifactFamily; round: number; name: string } | null {
|
|
40
|
+
const match = /^(.+?)(?:-r([1-9]\d*))?\.md$/.exec(name);
|
|
41
|
+
if (!match || match[0] !== name || !FAMILIES.has(match[1]!)) return null;
|
|
42
|
+
const round = match[2] ? Number(match[2]) : 1;
|
|
43
|
+
if (!Number.isSafeInteger(round) || (match[2] && round < 2)) return null;
|
|
44
|
+
return { family: match[1] as ArtifactFamily, round, name };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function maxArtifactRound(names: readonly string[], family: string): number {
|
|
48
|
+
return names.reduce((max, name) => {
|
|
49
|
+
const identity = parseArtifactName(name);
|
|
50
|
+
return identity?.family === family ? Math.max(max, identity.round) : max;
|
|
51
|
+
}, 0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { ARTIFACT_FAMILY_CATALOG };
|
|
55
|
+
export type { ArtifactFamily, ArtifactFamilySpec };
|