@cassiomc1/forgeloop 1.12.0 → 1.14.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/.github/copilot-instructions.md +1 -1
- package/AGENTS.md +1 -1
- package/AGENT_COMPATIBILITY.md +8 -0
- package/CLAUDE.md +1 -1
- package/CONTRIBUTING.md +90 -0
- package/DOCS_INDEX.md +46 -12
- package/ENG/c-development-eng.md +112 -0
- package/ENG/cpp-development-eng.md +109 -0
- package/ENG/dotnet-aspnetcore-development-eng.md +401 -0
- package/ENG/go-development-eng.md +103 -0
- package/ENG/java-development-eng.md +125 -0
- package/ENG/nodejs-backend-development-eng.md +605 -0
- package/ENG/php-development-eng.md +104 -0
- package/ENG/rust-development-eng.md +422 -0
- package/ENG/sec-code-eng.md +7 -7
- package/ENG/sql-development-eng.md +108 -0
- package/ENG/swift-development-eng.md +111 -0
- package/ENG/typescript-development-eng.md +108 -0
- package/EXECUTION_STATE.md +12 -0
- package/GUIDE_ROUTER.md +418 -9
- package/LOOP_ENGINEERING.md +28 -2
- package/ORCHESTRATOR_INTEGRATION.md +9 -5
- package/PROTOCOL_INTEGRATION.md +55 -2
- package/QUALITY_SCORECARD.md +1 -0
- package/README.md +78 -52
- package/TERMINOLOGY.md +2 -0
- package/THIRD_PARTY_NOTICES.md +19 -7
- package/THREAT_MODEL.md +140 -1
- package/completions/_forgeloop +22 -4
- package/completions/forgeloop.bash +40 -4
- package/completions/forgeloop.fish +130 -1
- package/docs/ADVISORY_CONTEXT.md +25 -0
- package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
- package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +81 -3
- package/docs/AGENT_SKILL.md +66 -0
- package/docs/ARTIFACT_REFERENCE.md +123 -0
- package/docs/AUDIT_UX.md +46 -0
- package/docs/BROWSER_VERIFICATION.md +136 -0
- package/docs/CLI_REFERENCE.md +392 -10
- package/docs/CODE_ATTESTATION.md +2 -2
- package/docs/DOCUMENTATION_GUIDE.md +34 -12
- package/docs/GETTING_STARTED.md +59 -0
- package/docs/JEV_BENCHMARKS.md +31 -0
- package/docs/MODEL_ROUTING.md +37 -0
- package/docs/OPENSRC_ADAPTER.md +241 -0
- package/docs/PACKAGE_CONTENTS.md +60 -19
- package/docs/PROVIDERS.md +126 -0
- package/docs/PROVIDER_ARCHITECTURE.md +199 -0
- package/docs/RECIPES.md +32 -0
- package/docs/RELEASE_CHECKLIST.md +66 -5
- package/docs/SECURITY_REVIEW.md +71 -0
- package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
- package/docs/TEST_INTELLIGENCE.md +29 -0
- package/docs/TEST_PRUNING.md +14 -0
- package/docs/TROUBLESHOOTING.md +298 -3
- package/docs/UNIVERSAL_INTEGRATION.md +31 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
- package/docs/diagrams/README.md +13 -9
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
- package/docs/documentation-manifest.json +1397 -0
- package/docs/protocol-requirements.json +101 -0
- package/package.json +46 -4
- package/schemas/config.schema.json +14 -0
- package/schemas/context-plan.schema.json +18 -0
- package/schemas/routing-input.schema.json +1 -1
- package/schemas/semantic-decision.schema.json +46 -0
- package/schemas/test-utility.schema.json +44 -0
- package/scripts/CI_VALIDATORS.md +84 -11
- package/scripts/benchmark-jev.mjs +5 -0
- package/scripts/benchmark-test-intelligence.mjs +4 -0
- package/scripts/generate-agent-protocol-summary.mjs +40 -1
- package/scripts/generate-forgeloop-skill.mjs +133 -0
- package/scripts/jev-smoke.mjs +19 -0
- package/skills/forgeloop/README.md +9 -0
- package/skills/forgeloop/SKILL.md +77 -0
- package/skills/forgeloop/references/lifecycle.md +9 -0
- package/skills/forgeloop/references/recovery.md +7 -0
- package/skills/forgeloop/references/verification.md +7 -0
- package/src/adapters/agent-browser/assertions.js +47 -0
- package/src/adapters/agent-browser/commands.js +54 -0
- package/src/adapters/agent-browser/index.js +3 -0
- package/src/adapters/agent-browser/locator.js +40 -0
- package/src/adapters/agent-browser/process.js +215 -0
- package/src/adapters/agent-browser/provider.js +313 -0
- package/src/adapters/emulated-services/constants.js +24 -0
- package/src/adapters/emulated-services/index.js +7 -0
- package/src/adapters/emulated-services/process.js +162 -0
- package/src/adapters/emulated-services/provider.js +282 -0
- package/src/adapters/opensrc/normalize.js +90 -0
- package/src/adapters/opensrc/process.js +248 -0
- package/src/adapters/opensrc/provider.js +338 -0
- package/src/adapters/opensrc/search.js +264 -0
- package/src/adapters/typesafe/client.js +28 -0
- package/src/adapters/typesafe/engine.js +63 -0
- package/src/adapters/typesafe/normalize.js +41 -0
- package/src/cli.js +108 -0
- package/src/commands/checkpoint-revalidate.js +176 -0
- package/src/commands/context-plan.js +38 -0
- package/src/commands/contract-create.js +264 -0
- package/src/commands/contract-revise.js +236 -0
- package/src/commands/decision-show.js +14 -0
- package/src/commands/decision-status.js +22 -0
- package/src/commands/discover.js +41 -0
- package/src/commands/doctor.js +15 -0
- package/src/commands/gate-record.js +205 -0
- package/src/commands/gate-revalidate.js +137 -0
- package/src/commands/model-route.js +32 -0
- package/src/commands/next.js +19 -7
- package/src/commands/route.js +146 -18
- package/src/commands/semantic-plan.js +17 -0
- package/src/commands/task-abandon.js +224 -0
- package/src/commands/task-create.js +84 -25
- package/src/commands/task-list.js +22 -2
- package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
- package/src/commands/task-repair-contract-bootstrap.js +263 -0
- package/src/commands/test-inventory.js +5 -0
- package/src/commands/test-prune-plan.js +5 -0
- package/src/commands/test-prune-probe.js +5 -0
- package/src/commands/test-utility.js +5 -0
- package/src/commands/validate-protocol.js +10 -1
- package/src/config/guides.json +44 -0
- package/src/core/artifact-registry.js +24 -0
- package/src/core/audit-ux.js +514 -0
- package/src/core/browser-verification/constants.js +149 -0
- package/src/core/browser-verification/normalize.js +254 -0
- package/src/core/browser-verification/provider.js +519 -0
- package/src/core/browser-verification/service.js +115 -0
- package/src/core/build-script.js +151 -0
- package/src/core/c-cpp-project.js +143 -0
- package/src/core/checkpoint-revalidation.js +319 -0
- package/src/core/cli-command-definitions.js +249 -1
- package/src/core/command-executors.js +115 -3
- package/src/core/command-input.js +212 -102
- package/src/core/completion-artifacts.js +14 -5
- package/src/core/completion.js +4 -6
- package/src/core/config.js +3 -0
- package/src/core/context-compiler/budget.js +9 -0
- package/src/core/context-compiler/candidates.js +39 -0
- package/src/core/context-compiler/compiler.js +63 -0
- package/src/core/context-compiler/fingerprint.js +11 -0
- package/src/core/context-compiler/policy.js +13 -0
- package/src/core/context-compiler/result.js +23 -0
- package/src/core/contract-bootstrap-recovery.js +655 -0
- package/src/core/contract-presets.js +82 -0
- package/src/core/contract-revision.js +210 -0
- package/src/core/decision/artifact.js +69 -0
- package/src/core/decision/benchmarks.js +103 -0
- package/src/core/decision/cache.js +27 -0
- package/src/core/decision/constants.js +58 -0
- package/src/core/decision/cutover.js +34 -0
- package/src/core/decision/engine.js +22 -0
- package/src/core/decision/errors.js +68 -0
- package/src/core/decision/events.js +101 -0
- package/src/core/decision/freshness.js +19 -0
- package/src/core/decision/normalizers/index.js +115 -0
- package/src/core/decision/policy.js +18 -0
- package/src/core/decision/projection.js +16 -0
- package/src/core/decision/question-registry.js +201 -0
- package/src/core/decision/request.js +26 -0
- package/src/core/decision/resolver.js +130 -0
- package/src/core/decision/result.js +58 -0
- package/src/core/decision/service.js +156 -0
- package/src/core/decision/state-builder.js +65 -0
- package/src/core/decision/task-bindings.js +30 -0
- package/src/core/decision/test-provider.js +32 -0
- package/src/core/decision/thresholds.js +15 -0
- package/src/core/error-codes.js +281 -3
- package/src/core/events.js +226 -57
- package/src/core/evidence-readiness.js +9 -0
- package/src/core/execution-prerequisites.js +14 -0
- package/src/core/execution-profile.js +63 -38
- package/src/core/filesystem.js +1 -10
- package/src/core/gate-provenance.js +124 -0
- package/src/core/go-project.js +206 -0
- package/src/core/integration-invocation-policy.js +27 -4
- package/src/core/integration-resources.js +86 -61
- package/src/core/java-project.js +403 -0
- package/src/core/model-router/constants.js +10 -0
- package/src/core/model-router/policy.js +103 -0
- package/src/core/model-router/router.js +37 -0
- package/src/core/multi-language-project.js +117 -0
- package/src/core/next-action-model.js +58 -0
- package/src/core/next-action-phases.js +130 -42
- package/src/core/next-action-refresh.js +43 -9
- package/src/core/next-action-review-phase.js +7 -2
- package/src/core/next-action.js +35 -7
- package/src/core/next-explanation.js +63 -0
- package/src/core/phase.js +128 -10
- package/src/core/php-project.js +85 -0
- package/src/core/preflight-consistency.js +23 -9
- package/src/core/preflight-loaders.js +37 -5
- package/src/core/project-detection.js +1760 -52
- package/src/core/protocol-info.js +65 -0
- package/src/core/protocol.js +20 -0
- package/src/core/reconcile-closure.js +132 -53
- package/src/core/recovery-history.js +1 -0
- package/src/core/resumability.js +154 -44
- package/src/core/route-artifact.js +15 -1
- package/src/core/router.js +223 -4
- package/src/core/runtime-context.js +118 -61
- package/src/core/rust-project.js +400 -0
- package/src/core/schema-validation.js +3 -0
- package/src/core/security-review/constants.js +64 -0
- package/src/core/security-review/normalize.js +245 -0
- package/src/core/security-review/provider.js +204 -0
- package/src/core/security-review/service.js +134 -0
- package/src/core/semantic-planning/constants.js +19 -0
- package/src/core/semantic-planning/projection.js +94 -0
- package/src/core/semantic-planning/service.js +15 -0
- package/src/core/sources.js +37 -0
- package/src/core/sql-project.js +141 -0
- package/src/core/swift-project.js +200 -0
- package/src/core/task-claim-state.js +201 -1
- package/src/core/task-conflict-inspection.js +31 -5
- package/src/core/task-paths.js +13 -0
- package/src/core/task-recovery.js +1 -0
- package/src/core/templates.js +3 -0
- package/src/core/test-intelligence/benchmarks.js +68 -0
- package/src/core/test-intelligence/inventory.js +73 -0
- package/src/core/test-intelligence/prune.js +90 -0
- package/src/core/test-intelligence/semantic-state.js +15 -0
- package/src/core/test-intelligence/service.js +40 -0
- package/src/core/test-intelligence/utility.js +50 -0
- package/src/core/trace.js +11 -7
- package/src/core/transaction.js +1 -0
- package/src/core/typescript-project.js +349 -0
- package/src/core/xml-structure.js +123 -0
- package/src/integration.d.ts +492 -0
- package/src/integration.js +54 -0
- package/src/providers/README.md +47 -0
- package/src/providers/capabilities.js +46 -0
- package/src/providers/errors.js +15 -0
- package/src/providers/index.js +29 -0
- package/src/providers/json-snapshot.js +105 -0
- package/src/providers/registry.js +152 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { resolveTaskContext } from "../core/task-context.js";
|
|
4
|
+
import { inspectTaskConflictState } from "../core/task-conflict-inspection.js";
|
|
5
|
+
import { appendProtocolEvent } from "../core/events.js";
|
|
6
|
+
import { withTaskTransaction } from "../core/transaction.js";
|
|
7
|
+
import { currentRepositoryFingerprint } from "../core/repository.js";
|
|
8
|
+
import { withProjectClaimsLock } from "../core/task-lock.js";
|
|
9
|
+
import { readTaskDescriptor } from "../core/task-descriptor.js";
|
|
10
|
+
import { createTaskRecovery, writeTaskRecovery } from "../core/task-recovery.js";
|
|
11
|
+
import { resolveTaskClaimState } from "../core/task-claim-state.js";
|
|
12
|
+
import {
|
|
13
|
+
E_TASK_ABANDON_AUTHORIZATION_REQUIRED,
|
|
14
|
+
E_TASK_ABANDON_INCONSISTENT,
|
|
15
|
+
E_TASK_ABANDON_INVALID_STATE,
|
|
16
|
+
E_TASK_ABANDON_UNSAFE,
|
|
17
|
+
E_TASK_ALREADY_ABANDONED,
|
|
18
|
+
} from "../core/error-codes.js";
|
|
19
|
+
|
|
20
|
+
const ABANDONMENT_REASON_CODES = Object.freeze(["CALLER_ABANDONED"]);
|
|
21
|
+
|
|
22
|
+
function abandonError(code, message, details = {}) {
|
|
23
|
+
const error = new Error(message);
|
|
24
|
+
error.code = code;
|
|
25
|
+
Object.assign(error, details);
|
|
26
|
+
return error;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sameList(left, right) {
|
|
30
|
+
return Array.isArray(left)
|
|
31
|
+
&& Array.isArray(right)
|
|
32
|
+
&& left.length === right.length
|
|
33
|
+
&& left.every((value, index) => value === right[index]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function snapshot(inspection) {
|
|
37
|
+
return {
|
|
38
|
+
phase: inspection.evidence.phase,
|
|
39
|
+
revision: inspection.evidence.workStateRevision,
|
|
40
|
+
ledgerLastSeq: inspection.evidence.ledgerLastSeq,
|
|
41
|
+
claimState: inspection.evidence.claimState,
|
|
42
|
+
effectiveWriteClaims: [...inspection.evidence.effectiveWriteClaims],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function assertSnapshotUnchanged(before, after, taskId) {
|
|
47
|
+
const unchanged = before.phase === after.phase
|
|
48
|
+
&& before.revision === after.revision
|
|
49
|
+
&& before.ledgerLastSeq === after.ledgerLastSeq
|
|
50
|
+
&& before.claimState === after.claimState
|
|
51
|
+
&& sameList(before.effectiveWriteClaims, after.effectiveWriteClaims);
|
|
52
|
+
if (!unchanged) {
|
|
53
|
+
throw abandonError(
|
|
54
|
+
E_TASK_ABANDON_UNSAFE,
|
|
55
|
+
`Task ${taskId} changed during abandonment precondition validation`,
|
|
56
|
+
{ before, after },
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function assertAbandonableOwnership(target, taskId, packageRoot) {
|
|
62
|
+
const projection = await resolveTaskClaimState(target, { taskId, packageRoot });
|
|
63
|
+
if (projection.phase === "COMPLETE" || projection.claimState === "RELEASED_BY_COMPLETION") {
|
|
64
|
+
throw abandonError(E_TASK_ABANDON_INVALID_STATE, `Task ${taskId} is COMPLETE and cannot be abandoned`);
|
|
65
|
+
}
|
|
66
|
+
if (!projection.valid || !projection.ownershipValid) {
|
|
67
|
+
throw abandonError(
|
|
68
|
+
E_TASK_ABANDON_INCONSISTENT,
|
|
69
|
+
`Task ${taskId} claim ownership is inconsistent; abandonment is blocked`,
|
|
70
|
+
{ reasonCodes: projection.reasonCodes, ownershipErrors: projection.ownershipErrors },
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (projection.claimState === "RELEASED_BY_RECOVERY") {
|
|
74
|
+
throw abandonError(
|
|
75
|
+
E_TASK_ALREADY_ABANDONED,
|
|
76
|
+
`Task ${taskId} is already abandoned/recovered`,
|
|
77
|
+
{ recovery: projection.recovery },
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (projection.claimState !== "ACTIVE" || projection.mutationAllowed !== true) {
|
|
81
|
+
throw abandonError(E_TASK_ABANDON_INVALID_STATE, `Task ${taskId} is not an active mutable task`);
|
|
82
|
+
}
|
|
83
|
+
return projection;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function assertInspectionAbandonable(taskId, inspection) {
|
|
87
|
+
if (!inspection.evidence.healthy || !inspection.evidence.ledgerValid || !inspection.evidence.ownershipValid) {
|
|
88
|
+
throw abandonError(
|
|
89
|
+
E_TASK_ABANDON_INCONSISTENT,
|
|
90
|
+
`Task ${taskId} cannot be abandoned because canonical ownership or ledger validation failed`,
|
|
91
|
+
{ reasonCodes: inspection.reasonCodes, ledgerErrors: inspection.evidence.ledgerErrors },
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (!inspection.evidence.phase || inspection.evidence.phase === "COMPLETE") {
|
|
95
|
+
throw abandonError(E_TASK_ABANDON_INVALID_STATE, `Task ${taskId} is not eligible for abandonment`);
|
|
96
|
+
}
|
|
97
|
+
if (inspection.evidence.claimState !== "ACTIVE" || inspection.evidence.mutationAllowed !== true) {
|
|
98
|
+
throw abandonError(E_TASK_ABANDON_INVALID_STATE, `Task ${taskId} is not an active mutable task`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function runTaskAbandon({
|
|
103
|
+
target,
|
|
104
|
+
packageRoot,
|
|
105
|
+
taskId,
|
|
106
|
+
acknowledgeAbandonment = false,
|
|
107
|
+
} = {}) {
|
|
108
|
+
if (typeof taskId !== "string" || taskId.trim() === "") {
|
|
109
|
+
throw abandonError(E_TASK_ABANDON_INVALID_STATE, "task-abandon requires an explicit --task ID");
|
|
110
|
+
}
|
|
111
|
+
if (!acknowledgeAbandonment) {
|
|
112
|
+
throw abandonError(
|
|
113
|
+
E_TASK_ABANDON_AUTHORIZATION_REQUIRED,
|
|
114
|
+
"task-abandon requires explicit caller acknowledgement: re-run with --acknowledge-abandonment",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let context;
|
|
119
|
+
try {
|
|
120
|
+
context = await resolveTaskContext(target, { taskId, packageRoot, explicitRequired: true });
|
|
121
|
+
} catch (error) {
|
|
122
|
+
throw abandonError(
|
|
123
|
+
E_TASK_ABANDON_INCONSISTENT,
|
|
124
|
+
`Task ${taskId} cannot be abandoned because its canonical identity is unreadable`,
|
|
125
|
+
{ causeCode: error.code, causeMessage: error.message },
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const effectiveTaskId = context.taskId;
|
|
129
|
+
await assertAbandonableOwnership(target, effectiveTaskId, packageRoot);
|
|
130
|
+
|
|
131
|
+
return withProjectClaimsLock(target, "task-abandon", async () => {
|
|
132
|
+
await assertAbandonableOwnership(target, effectiveTaskId, packageRoot);
|
|
133
|
+
const inspectionBeforeLock = await inspectTaskConflictState(target, {
|
|
134
|
+
taskId: effectiveTaskId,
|
|
135
|
+
packageRoot,
|
|
136
|
+
});
|
|
137
|
+
assertInspectionAbandonable(effectiveTaskId, inspectionBeforeLock);
|
|
138
|
+
const before = snapshot(inspectionBeforeLock);
|
|
139
|
+
|
|
140
|
+
return withTaskTransaction({
|
|
141
|
+
target,
|
|
142
|
+
taskId: effectiveTaskId,
|
|
143
|
+
operation: "task-abandon",
|
|
144
|
+
packageRoot,
|
|
145
|
+
recordCommitEvent: true,
|
|
146
|
+
}, async (transaction) => {
|
|
147
|
+
const descriptor = await readTaskDescriptor(target, context.taskKey, packageRoot);
|
|
148
|
+
if (descriptor.value.taskId !== effectiveTaskId || descriptor.value.taskKey !== context.taskKey) {
|
|
149
|
+
throw abandonError(E_TASK_ABANDON_UNSAFE, `Task ${effectiveTaskId} identity changed during abandonment`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await assertAbandonableOwnership(target, effectiveTaskId, packageRoot);
|
|
153
|
+
const inspection = await inspectTaskConflictState(target, {
|
|
154
|
+
taskId: effectiveTaskId,
|
|
155
|
+
packageRoot,
|
|
156
|
+
ignoredLockId: transaction.lock.lockId,
|
|
157
|
+
});
|
|
158
|
+
assertInspectionAbandonable(effectiveTaskId, inspection);
|
|
159
|
+
assertSnapshotUnchanged(before, snapshot(inspection), effectiveTaskId);
|
|
160
|
+
|
|
161
|
+
const repository = await currentRepositoryFingerprint(target);
|
|
162
|
+
const recoveryId = `recovery-${randomUUID()}`;
|
|
163
|
+
const abandonedAt = new Date().toISOString();
|
|
164
|
+
const releasedClaims = [...inspection.evidence.effectiveWriteClaims];
|
|
165
|
+
const details = {
|
|
166
|
+
recoveryId,
|
|
167
|
+
classification: "ABANDONED",
|
|
168
|
+
reasonCodes: [...ABANDONMENT_REASON_CODES],
|
|
169
|
+
previousPhase: inspection.evidence.phase,
|
|
170
|
+
previousRevision: inspection.evidence.workStateRevision,
|
|
171
|
+
previousHead: inspection.evidence.repositoryHead,
|
|
172
|
+
previousBranch: inspection.evidence.repositoryBranch,
|
|
173
|
+
currentHead: repository.head,
|
|
174
|
+
currentBranch: repository.branch,
|
|
175
|
+
releasedClaims,
|
|
176
|
+
authorityKind: "CALLER_ACKNOWLEDGED",
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const abandonmentEvent = await appendProtocolEvent(target, {
|
|
180
|
+
taskId: effectiveTaskId,
|
|
181
|
+
event: "TASK_ABANDONED",
|
|
182
|
+
at: abandonedAt,
|
|
183
|
+
details,
|
|
184
|
+
}, packageRoot, { taskId: effectiveTaskId });
|
|
185
|
+
|
|
186
|
+
await writeTaskRecovery(target, createTaskRecovery({
|
|
187
|
+
taskId: effectiveTaskId,
|
|
188
|
+
recoveredAt: abandonedAt,
|
|
189
|
+
recoveryId,
|
|
190
|
+
recoveryEventSeq: abandonmentEvent.seq,
|
|
191
|
+
classificationAtRecovery: "ABANDONED",
|
|
192
|
+
reasonCodes: ABANDONMENT_REASON_CODES,
|
|
193
|
+
releasedClaims,
|
|
194
|
+
previousPhase: inspection.evidence.phase,
|
|
195
|
+
previousRevision: inspection.evidence.workStateRevision,
|
|
196
|
+
repositoryFingerprint: repository,
|
|
197
|
+
authority: { kind: "CALLER_ACKNOWLEDGED" },
|
|
198
|
+
}), packageRoot);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
taskId: effectiveTaskId,
|
|
202
|
+
taskKey: context.taskKey,
|
|
203
|
+
abandoned: true,
|
|
204
|
+
recovered: true,
|
|
205
|
+
recoveryId,
|
|
206
|
+
classification: "ABANDONED",
|
|
207
|
+
reasonCodes: [...ABANDONMENT_REASON_CODES],
|
|
208
|
+
phase: inspection.evidence.phase,
|
|
209
|
+
claimsReleased: true,
|
|
210
|
+
releasedClaims,
|
|
211
|
+
effectiveWriteClaims: [],
|
|
212
|
+
claimState: "RELEASED_BY_RECOVERY",
|
|
213
|
+
mutationAllowed: false,
|
|
214
|
+
authority: { kind: "CALLER_ACKNOWLEDGED" },
|
|
215
|
+
message: `Task ${effectiveTaskId} explicitly abandoned by caller acknowledgement; write claims released without completion claim`,
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function formatTaskAbandonResult(result) {
|
|
222
|
+
const claims = result.releasedClaims.length === 0 ? "none" : result.releasedClaims.join(", ");
|
|
223
|
+
return `${result.message}\nclassification: ${result.classification}\nphase: ${result.phase} (unchanged)\nreleased claims: ${claims}\n`;
|
|
224
|
+
}
|
|
@@ -9,6 +9,7 @@ import { withTaskTransaction } from "../core/transaction.js";
|
|
|
9
9
|
import { taskDirectory } from "../core/task-paths.js";
|
|
10
10
|
import { ensureWithin, fileExists } from "../core/filesystem.js";
|
|
11
11
|
import { validateContract, writeContract } from "../core/contract.js";
|
|
12
|
+
import { createPresetContract, CONTRACT_PRESET_IDS } from "../core/contract-presets.js";
|
|
12
13
|
import { appendProtocolEvent } from "../core/events.js";
|
|
13
14
|
import { E_TASK_REQUIRED, E_TASK_ALREADY_EXISTS, E_TASK_DESCRIPTOR_INVALID } from "../core/error-codes.js";
|
|
14
15
|
import { recoveryGuidanceForClassification } from "../core/next-action-model.js";
|
|
@@ -56,12 +57,45 @@ export async function assertNoScopeConflictsWithInspection(claims, existingTasks
|
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
async function validateTaskCreateScope({ target, packageRoot, claims, taskId }) {
|
|
61
|
+
const allTasks = await discoverTasks(target, packageRoot);
|
|
62
|
+
await assertNoScopeConflictsWithInspection(claims, allTasks, taskId, { target, packageRoot });
|
|
63
|
+
if (claims.length > 0) await assertScopeClean(target, claims);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function previewResult({ taskId, normalizedClaims, preset, proposedContract }) {
|
|
67
|
+
return {
|
|
68
|
+
preview: true,
|
|
69
|
+
taskId,
|
|
70
|
+
writeClaims: normalizedClaims,
|
|
71
|
+
preset,
|
|
72
|
+
contract: proposedContract,
|
|
73
|
+
createsLifecycleState: false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function runTaskCreate({
|
|
78
|
+
target,
|
|
79
|
+
packageRoot,
|
|
80
|
+
taskId,
|
|
81
|
+
claims = [],
|
|
82
|
+
contractFile = null,
|
|
83
|
+
preset = null,
|
|
84
|
+
preview = false,
|
|
85
|
+
} = {}) {
|
|
60
86
|
if (!taskId) {
|
|
61
87
|
throw taskError(E_TASK_REQUIRED, "--task is required for task-create");
|
|
62
88
|
}
|
|
63
89
|
assertTaskId(taskId);
|
|
64
90
|
|
|
91
|
+
if (preset && !CONTRACT_PRESET_IDS.includes(preset)) {
|
|
92
|
+
const error = new Error(`Unknown contract preset: ${preset}. Expected one of ${CONTRACT_PRESET_IDS.join(", ")}`);
|
|
93
|
+
error.code = "E_CONTRACT_PRESET_UNKNOWN";
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
if (preset && contractFile) {
|
|
97
|
+
throw taskError("E_CONTRACT_INPUT_CONFLICT", "Use either --preset or --contract-file, not both");
|
|
98
|
+
}
|
|
65
99
|
const existing = await findTaskById(target, taskId, packageRoot);
|
|
66
100
|
if (existing) {
|
|
67
101
|
throw taskError(E_TASK_ALREADY_EXISTS, `Task already exists: ${taskId}`);
|
|
@@ -69,13 +103,45 @@ export async function runTaskCreate({ target, packageRoot, taskId, claims = [],
|
|
|
69
103
|
|
|
70
104
|
const normalizedClaims = normalizeWriteClaims(claims ?? []);
|
|
71
105
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
106
|
+
let proposedContract = null;
|
|
107
|
+
if (preset) proposedContract = createPresetContract({ taskId, preset, claims: normalizedClaims });
|
|
108
|
+
if (contractFile) {
|
|
109
|
+
const sourcePath = ensureWithin(target, contractFile);
|
|
110
|
+
if (!(await fileExists(sourcePath))) {
|
|
111
|
+
throw taskError("E_CONTRACT_MISSING", `Specified contract file not found: ${contractFile}`);
|
|
112
|
+
}
|
|
113
|
+
const raw = await readFile(sourcePath, "utf8");
|
|
114
|
+
try {
|
|
115
|
+
proposedContract = JSON.parse(raw);
|
|
116
|
+
} catch {
|
|
117
|
+
throw taskError("E_CONTRACT_INVALID", `Specified contract file is not valid JSON: ${contractFile}`);
|
|
77
118
|
}
|
|
119
|
+
await validateContract(proposedContract, packageRoot);
|
|
120
|
+
if (proposedContract.taskId && proposedContract.taskId !== taskId) {
|
|
121
|
+
throw taskError(
|
|
122
|
+
E_TASK_DESCRIPTOR_INVALID,
|
|
123
|
+
`Contract taskId "${proposedContract.taskId}" does not match requested taskId "${taskId}"`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
proposedContract = { ...proposedContract, taskId };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (proposedContract) await validateContract({ ...proposedContract, taskId }, packageRoot);
|
|
130
|
+
|
|
131
|
+
// Preview is advisory: it must never create the claims-lock directory, remove
|
|
132
|
+
// a stale lock, or fail because another process currently owns the lock.
|
|
133
|
+
if (preview) {
|
|
134
|
+
await validateTaskCreateScope({ target, packageRoot, claims: normalizedClaims, taskId });
|
|
135
|
+
return previewResult({ taskId, normalizedClaims, preset, proposedContract });
|
|
136
|
+
}
|
|
78
137
|
|
|
138
|
+
return withProjectClaimsLock(target, async () => {
|
|
139
|
+
// Re-check after acquiring the reservation. Preview results are not a
|
|
140
|
+
// reservation and the repository may have changed since they were built.
|
|
141
|
+
if (await findTaskById(target, taskId, packageRoot)) {
|
|
142
|
+
throw taskError(E_TASK_ALREADY_EXISTS, `Task already exists: ${taskId}`);
|
|
143
|
+
}
|
|
144
|
+
await validateTaskCreateScope({ target, packageRoot, claims: normalizedClaims, taskId });
|
|
79
145
|
return withTaskTransaction({ target, taskId, operation: "task-create", packageRoot, recordCommitEvent: true }, async () => {
|
|
80
146
|
const descriptor = createTaskDescriptor({
|
|
81
147
|
taskId,
|
|
@@ -84,26 +150,9 @@ export async function runTaskCreate({ target, packageRoot, taskId, claims = [],
|
|
|
84
150
|
const written = await writeTaskDescriptor(target, descriptor, packageRoot);
|
|
85
151
|
|
|
86
152
|
let contractCopied = false;
|
|
87
|
-
if (
|
|
88
|
-
const
|
|
89
|
-
if (!(await fileExists(sourcePath))) {
|
|
90
|
-
throw taskError("E_CONTRACT_MISSING", `Specified contract file not found: ${contractFile}`);
|
|
91
|
-
}
|
|
92
|
-
const raw = await readFile(sourcePath, "utf8");
|
|
93
|
-
let parsed;
|
|
94
|
-
try {
|
|
95
|
-
parsed = JSON.parse(raw);
|
|
96
|
-
} catch {
|
|
97
|
-
throw taskError("E_CONTRACT_INVALID", `Specified contract file is not valid JSON: ${contractFile}`);
|
|
98
|
-
}
|
|
153
|
+
if (proposedContract) {
|
|
154
|
+
const parsed = { ...proposedContract, taskId };
|
|
99
155
|
await validateContract(parsed, packageRoot);
|
|
100
|
-
if (parsed.taskId && parsed.taskId !== taskId) {
|
|
101
|
-
throw taskError(
|
|
102
|
-
E_TASK_DESCRIPTOR_INVALID,
|
|
103
|
-
`Contract taskId "${parsed.taskId}" does not match requested taskId "${taskId}"`,
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
parsed.taskId = taskId;
|
|
107
156
|
await writeContract(target, parsed, packageRoot, { taskId });
|
|
108
157
|
contractCopied = true;
|
|
109
158
|
}
|
|
@@ -128,6 +177,16 @@ export async function runTaskCreate({ target, packageRoot, taskId, claims = [],
|
|
|
128
177
|
}
|
|
129
178
|
|
|
130
179
|
export function formatTaskCreateResult(result) {
|
|
180
|
+
if (result.preview) {
|
|
181
|
+
return `${JSON.stringify({
|
|
182
|
+
preview: true,
|
|
183
|
+
taskId: result.taskId,
|
|
184
|
+
preset: result.preset,
|
|
185
|
+
writeClaims: result.writeClaims,
|
|
186
|
+
contract: result.contract,
|
|
187
|
+
createsLifecycleState: false,
|
|
188
|
+
}, null, 2)}\n`;
|
|
189
|
+
}
|
|
131
190
|
const claims = result.writeClaims.length === 0 ? "none" : result.writeClaims.join(", ");
|
|
132
191
|
return `created task: ${result.taskId}\nkey: ${result.taskKey}\ndirectory: ${result.directory}\nclaims: ${claims}\n`;
|
|
133
192
|
}
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { discoverTasks } from "../core/task-discovery.js";
|
|
2
|
+
import { WORK_PHASES } from "../core/protocol.js";
|
|
2
3
|
|
|
3
|
-
export async function runTaskList({ target, packageRoot } = {}) {
|
|
4
|
+
export async function runTaskList({ target, packageRoot, phase = null, active = false, limit = null, offset = 0 } = {}) {
|
|
4
5
|
const tasks = await discoverTasks(target, packageRoot);
|
|
6
|
+
const normalizedPhase = typeof phase === "string" && phase.trim() !== "" ? phase.trim().toUpperCase() : null;
|
|
7
|
+
if (normalizedPhase && !WORK_PHASES.includes(normalizedPhase)) {
|
|
8
|
+
const error = new Error(`Unknown task phase filter: ${phase}`);
|
|
9
|
+
error.code = "E_TASK_PHASE_INVALID";
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
const phaseFiltered = normalizedPhase ? tasks.filter((task) => task.phase === normalizedPhase) : tasks;
|
|
13
|
+
const filtered = active
|
|
14
|
+
? phaseFiltered.filter((task) => task.claimState === "ACTIVE" && task.mutationAllowed === true)
|
|
15
|
+
: phaseFiltered;
|
|
16
|
+
const start = Number.isInteger(offset) && offset >= 0 ? offset : 0;
|
|
17
|
+
const end = Number.isInteger(limit) && limit >= 0 ? start + limit : undefined;
|
|
18
|
+
const projected = filtered.slice(start, end);
|
|
5
19
|
return {
|
|
6
|
-
tasks:
|
|
20
|
+
tasks: projected.map((task) => {
|
|
7
21
|
if (task.healthy === false) {
|
|
8
22
|
return {
|
|
9
23
|
taskId: task.taskId ?? null,
|
|
@@ -35,6 +49,12 @@ export async function runTaskList({ target, packageRoot } = {}) {
|
|
|
35
49
|
updatedAt: task.updatedAt,
|
|
36
50
|
};
|
|
37
51
|
}),
|
|
52
|
+
...(normalizedPhase ? { phase: normalizedPhase } : {}),
|
|
53
|
+
...(active ? { active: true } : {}),
|
|
54
|
+
offset: start,
|
|
55
|
+
...(Number.isInteger(limit) && limit >= 0 ? { limit } : {}),
|
|
56
|
+
total: filtered.length,
|
|
57
|
+
hasMore: end !== undefined ? end < filtered.length : false,
|
|
38
58
|
};
|
|
39
59
|
}
|
|
40
60
|
|