@cassiomc1/forgeloop 1.10.0 → 1.10.2

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.
Files changed (67) hide show
  1. package/DOCS_INDEX.md +12 -1
  2. package/LOOP_ENGINEERING.md +4 -4
  3. package/LOOP_SYSTEM_DESIGN.md +23 -0
  4. package/QUALITY_SCORECARD.md +2 -2
  5. package/README.md +22 -8
  6. package/docs/ADVISORY_CONTEXT.md +24 -0
  7. package/docs/AGENT_PROTOCOL_SUMMARY.md +1 -1
  8. package/docs/ARTIFACT_REFERENCE.md +15 -0
  9. package/docs/CODE_ATTESTATION.md +9 -0
  10. package/docs/DOCUMENTATION_GUIDE.md +4 -4
  11. package/docs/EXECUTION_PROFILE_BENCHMARKS.md +10 -0
  12. package/docs/MCP.md +13 -1
  13. package/docs/PACKAGE_CONTENTS.md +88 -0
  14. package/docs/RELEASE_CHECKLIST.md +10 -2
  15. package/docs/REVISION_PROVIDERS.md +9 -0
  16. package/docs/RIPWIRE_ADAPTER.md +189 -0
  17. package/docs/TROUBLESHOOTING.md +12 -0
  18. package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +13 -2
  19. package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +6 -6
  20. package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +10 -1
  21. package/docs/assets/diagrams/forgeloop-engineering-flow.html +22 -11
  22. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  23. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +17 -8
  24. package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +14 -3
  25. package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +6 -6
  26. package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +10 -1
  27. package/docs/diagrams/README.md +26 -0
  28. package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +383 -57
  29. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +374 -55
  30. package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +328 -47
  31. package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
  32. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
  33. package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
  34. package/package.json +14 -4
  35. package/scripts/CI_VALIDATORS.md +24 -0
  36. package/scripts/check-critical-coverage.mjs +9 -0
  37. package/src/adapters/ripwire/normalize.js +352 -0
  38. package/src/adapters/ripwire/process.js +248 -0
  39. package/src/adapters/ripwire/provider.js +245 -0
  40. package/src/commands/doctor.js +11 -10
  41. package/src/core/actions.js +2 -2
  42. package/src/core/advisory-context/service.js +36 -14
  43. package/src/core/approvals.js +2 -2
  44. package/src/core/artifacts.js +3 -3
  45. package/src/core/checks.js +0 -33
  46. package/src/core/completion.js +2 -2
  47. package/src/core/events.js +5 -5
  48. package/src/core/execution-profile.js +18 -5
  49. package/src/core/handoff-acceptance.js +10 -1
  50. package/src/core/next-action-pending-actions.js +255 -0
  51. package/src/core/next-action-phases.js +26 -764
  52. package/src/core/next-action-planned-phase.js +51 -0
  53. package/src/core/next-action-quality-guidance.js +19 -0
  54. package/src/core/next-action-recovery-phases.js +97 -0
  55. package/src/core/next-action-refresh.js +20 -0
  56. package/src/core/next-action-review-phase.js +189 -0
  57. package/src/core/next-action-verification-phase.js +192 -0
  58. package/src/core/task-recovery.js +2 -2
  59. package/src/core/transaction-maintenance.js +70 -0
  60. package/src/core/transaction.js +31 -10
  61. package/src/core/work-state.js +20 -11
  62. package/src/integration.d.ts +30 -3
  63. package/src/integration.js +2 -0
  64. package/src/core/cli-metadata.js +0 -23
  65. package/src/core/decision-classification.js +0 -55
  66. package/src/core/gates.js +0 -57
  67. package/src/core/workflow-compatibility.js +0 -151
@@ -0,0 +1,245 @@
1
+ import path from "node:path";
2
+
3
+ import { assertSafePath } from "../../core/filesystem.js";
4
+ import {
5
+ normalizeAdvisoryRecallOptions,
6
+ } from "../../core/advisory-context/constants.js";
7
+ import {
8
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
9
+ E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
10
+ E_ADVISORY_CONTEXT_RESULT_INVALID,
11
+ E_ADVISORY_CONTEXT_TIMEOUT,
12
+ } from "../../core/error-codes.js";
13
+ import {
14
+ RIPWIRE_PROCESS_LIMITS,
15
+ parseRipwireVersion,
16
+ runRipwireCommand,
17
+ } from "./process.js";
18
+ import {
19
+ extractCandidateRows,
20
+ getRipwireRowPath,
21
+ normalizeReportedPath,
22
+ normalizeRipwireResult,
23
+ } from "./normalize.js";
24
+
25
+ function providerError(code, message) {
26
+ const error = new Error(message);
27
+ error.name = "RipwireAdvisoryProviderError";
28
+ error.code = code;
29
+ return error;
30
+ }
31
+
32
+ function assertAbsoluteExecutablePath(value) {
33
+ if (typeof value !== "string" || value.trim() === "" || !path.isAbsolute(value) || /\p{Cc}/u.test(value)) {
34
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "Ripwire executablePath must be an absolute portable path");
35
+ }
36
+ return value;
37
+ }
38
+
39
+ function assertExpectedVersion(value) {
40
+ if (
41
+ typeof value !== "string"
42
+ || value.trim() === ""
43
+ || value.length > 64
44
+ || !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/u.test(value)
45
+ ) {
46
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "Ripwire expectedVersion must be a qualified version token under 64 characters");
47
+ }
48
+ return value;
49
+ }
50
+
51
+ function assertProcessLimit(value, maximum, label) {
52
+ if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
53
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, `${label} must be an integer between 1 and ${maximum}`);
54
+ }
55
+ return value;
56
+ }
57
+
58
+ function remainingTimeout(deadline) {
59
+ const remaining = deadline - Date.now();
60
+ if (remaining < 1) {
61
+ throw providerError(E_ADVISORY_CONTEXT_TIMEOUT, "Ripwire advisory recall deadline expired");
62
+ }
63
+ return remaining;
64
+ }
65
+
66
+ function reportTransport(callback, payload) {
67
+ if (typeof callback !== "function") return;
68
+ try {
69
+ callback(Object.freeze({ ...payload }));
70
+ } catch {
71
+ // Observability hooks cannot change advisory correctness or failure mapping.
72
+ }
73
+ }
74
+
75
+ async function runAndReport(executablePath, args, {
76
+ cwd,
77
+ timeoutMs,
78
+ spawnImpl,
79
+ env,
80
+ maxStdoutBytes,
81
+ maxStderrBytes,
82
+ onTransport,
83
+ kind,
84
+ } = {}) {
85
+ const result = await runRipwireCommand(executablePath, args, {
86
+ cwd,
87
+ timeoutMs,
88
+ spawnImpl,
89
+ env,
90
+ maxStdoutBytes,
91
+ maxStderrBytes,
92
+ });
93
+ reportTransport(onTransport, {
94
+ kind,
95
+ stdoutBytes: result.stdoutBytes,
96
+ stderrBytes: result.stderrBytes,
97
+ durationMs: result.durationMs,
98
+ });
99
+ return result;
100
+ }
101
+
102
+ function queryArguments(projectPath, query) {
103
+ return [
104
+ projectPath,
105
+ `--for=${query}`,
106
+ "--signatures-only",
107
+ "--json",
108
+ "--no-cache",
109
+ "--exclude=.forgeloop",
110
+ ];
111
+ }
112
+
113
+ /**
114
+ * Create an optional host-injected Ripwire adapter.
115
+ *
116
+ * Construction is inert: it validates the explicit executable/version pair
117
+ * but does not discover binaries, spawn processes, access the network, or
118
+ * write lifecycle state. Version qualification is repeated lazily before a
119
+ * query so a changed executable cannot silently change the provider identity.
120
+ */
121
+ export function createRipwireAdvisoryContextProvider({
122
+ executablePath,
123
+ expectedVersion,
124
+ spawnImpl,
125
+ env,
126
+ maxStdoutBytes = RIPWIRE_PROCESS_LIMITS.maxStdoutBytes,
127
+ maxStderrBytes = RIPWIRE_PROCESS_LIMITS.maxStderrBytes,
128
+ onTransport,
129
+ } = {}) {
130
+ const qualifiedExecutablePath = assertAbsoluteExecutablePath(executablePath);
131
+ const qualifiedExpectedVersion = assertExpectedVersion(expectedVersion);
132
+ const qualifiedStdoutLimit = assertProcessLimit(maxStdoutBytes, RIPWIRE_PROCESS_LIMITS.maxStdoutBytes, "Ripwire stdout limit");
133
+ const qualifiedStderrLimit = assertProcessLimit(maxStderrBytes, RIPWIRE_PROCESS_LIMITS.maxStderrBytes, "Ripwire stderr limit");
134
+
135
+ const provider = {
136
+ id: "ripwire",
137
+ version: qualifiedExpectedVersion,
138
+ async recall({
139
+ projectPath,
140
+ taskId,
141
+ query,
142
+ limit,
143
+ maxItemChars,
144
+ maxTotalChars,
145
+ timeoutMs,
146
+ } = {}) {
147
+ if (typeof projectPath !== "string" || !path.isAbsolute(projectPath) || /\p{Cc}/u.test(projectPath)) {
148
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "Ripwire projectPath must be an absolute path");
149
+ }
150
+ if (typeof taskId !== "string" || taskId.trim() === "") {
151
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "Ripwire taskId must be a non-empty string");
152
+ }
153
+ if (typeof query !== "string" || query.trim() === "" || /\p{Cc}/u.test(query)) {
154
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "Ripwire query must be a non-empty portable string");
155
+ }
156
+ const options = normalizeAdvisoryRecallOptions({ limit, maxItemChars, maxTotalChars, timeoutMs });
157
+ const projectRoot = path.resolve(projectPath);
158
+ try {
159
+ await assertSafePath(projectRoot, ".");
160
+ } catch {
161
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE, "Ripwire target directory is unavailable or unsafe");
162
+ }
163
+
164
+ const deadline = Date.now() + options.timeoutMs;
165
+ let versionResult;
166
+ try {
167
+ versionResult = await runAndReport(qualifiedExecutablePath, ["--version"], {
168
+ cwd: projectRoot,
169
+ timeoutMs: remainingTimeout(deadline),
170
+ spawnImpl,
171
+ env,
172
+ maxStdoutBytes: Math.min(qualifiedStdoutLimit, 64 * 1024),
173
+ maxStderrBytes: qualifiedStderrLimit,
174
+ onTransport,
175
+ kind: "version",
176
+ });
177
+ } catch (error) {
178
+ throw error.code ? error : providerError(E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE, "Ripwire version probe failed");
179
+ }
180
+
181
+ let actualVersion;
182
+ try {
183
+ actualVersion = parseRipwireVersion(versionResult.stdout);
184
+ } catch {
185
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "Ripwire version probe was not qualified");
186
+ }
187
+ if (actualVersion !== qualifiedExpectedVersion) {
188
+ throw providerError(
189
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
190
+ "Ripwire executable version does not match the expected qualified version",
191
+ );
192
+ }
193
+
194
+ let queryResult;
195
+ try {
196
+ queryResult = await runAndReport(qualifiedExecutablePath, queryArguments(projectRoot, query), {
197
+ cwd: projectRoot,
198
+ timeoutMs: remainingTimeout(deadline),
199
+ spawnImpl,
200
+ env,
201
+ maxStdoutBytes: qualifiedStdoutLimit,
202
+ maxStderrBytes: qualifiedStderrLimit,
203
+ onTransport,
204
+ kind: "query",
205
+ });
206
+ } catch (error) {
207
+ throw error.code ? error : providerError(E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE, "Ripwire query failed");
208
+ }
209
+
210
+ let raw;
211
+ try {
212
+ raw = JSON.parse(queryResult.stdout);
213
+ } catch {
214
+ throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "Ripwire query did not return valid JSON");
215
+ }
216
+
217
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
218
+ throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "Ripwire query JSON root must be an object");
219
+ }
220
+
221
+ const safeSourcePaths = new Set();
222
+ const rows = extractCandidateRows(raw) ?? [];
223
+ for (const row of rows) {
224
+ const relativePath = normalizeReportedPath(getRipwireRowPath(row), projectRoot);
225
+ if (!relativePath) continue;
226
+ try {
227
+ await assertSafePath(projectRoot, relativePath);
228
+ safeSourcePaths.add(relativePath);
229
+ } catch {
230
+ // The normalizer will disclose and drop this candidate.
231
+ }
232
+ }
233
+
234
+ return normalizeRipwireResult(raw, {
235
+ projectPath: projectRoot,
236
+ ...options,
237
+ sourcePathValidator: (relativePath) => safeSourcePaths.has(relativePath),
238
+ });
239
+ },
240
+ };
241
+
242
+ return Object.freeze(provider);
243
+ }
244
+
245
+ export { queryArguments };
@@ -116,16 +116,7 @@ async function adoptAdapters({ target, manifest, adoptPaths, findings }) {
116
116
 
117
117
  export async function runDoctor({ target, packageRoot, adoptPaths = [], strict = false, fix = false }) {
118
118
  const findings = [];
119
- const incompleteTransactions = await findIncompleteTransactions(target);
120
- for (const transaction of incompleteTransactions) {
121
- findings.push(finding(
122
- "E_TRANSACTION_INCOMPLETE",
123
- "error",
124
- `.forgeloop/.txn/${transaction.transactionId}`,
125
- `Transaction ${transaction.transactionId} is ${transaction.status} and requires inspection or deterministic recovery.`,
126
- "Run forgeloop doctor --fix only after ensuring no active process owns the task lock.",
127
- ));
128
- }
119
+ let incompleteTransactions = await findIncompleteTransactions(target);
129
120
  if (fix && incompleteTransactions.some((transaction) => transaction.status === "COMMITTING")) {
130
121
  const recovered = await recoverIncompleteTransactions(target);
131
122
  for (const transaction of recovered) {
@@ -136,6 +127,16 @@ export async function runDoctor({ target, packageRoot, adoptPaths = [], strict =
136
127
  `Transaction recovery result: ${transaction.status}.`,
137
128
  ));
138
129
  }
130
+ incompleteTransactions = await findIncompleteTransactions(target);
131
+ }
132
+ for (const transaction of incompleteTransactions) {
133
+ findings.push(finding(
134
+ "E_TRANSACTION_INCOMPLETE",
135
+ "error",
136
+ `.forgeloop/.txn/${transaction.transactionId}`,
137
+ `Transaction ${transaction.transactionId} is ${transaction.status} and requires inspection or deterministic recovery.`,
138
+ "Run forgeloop doctor --fix only after ensuring no active process owns the task lock.",
139
+ ));
139
140
  }
140
141
  let manifest = null;
141
142
  let manifestChanged = false;
@@ -1,7 +1,7 @@
1
1
  import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
- import { getActiveTaskTransaction, withTaskTransaction } from "./transaction.js";
4
+ import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
5
5
  import { appendProtocolEvent, readEvents } from "./events.js";
6
6
  import {
7
7
  canonicalActionFingerprint,
@@ -63,7 +63,7 @@ async function writeActionFile(target, packageRoot, taskId, action) {
63
63
  const relPath = taskActionPath(taskId, action.actionId);
64
64
  await assertSafePath(target, relPath);
65
65
  const serialized = `${JSON.stringify(action, null, 2)}\n`;
66
- const activeTransaction = getActiveTaskTransaction();
66
+ const activeTransaction = (await getTaskTransaction(target));
67
67
  if (activeTransaction) {
68
68
  await activeTransaction.stageText(relPath, serialized);
69
69
  } else {
@@ -20,6 +20,8 @@ import {
20
20
  E_PORTABLE_CONTEXT_INVALID,
21
21
  } from "../error-codes.js";
22
22
 
23
+ const PROVIDER_CLEANUP_GRACE_MS = 1000;
24
+
23
25
  function serviceError(code, message, cause) {
24
26
  const error = new Error(message, cause !== undefined ? { cause } : undefined);
25
27
  error.name = "AdvisoryContextServiceError";
@@ -106,30 +108,50 @@ export async function recallAdvisoryContext({
106
108
  }
107
109
 
108
110
  let timer;
111
+ let cleanupTimer;
112
+ const timeoutError = serviceError(
113
+ E_ADVISORY_CONTEXT_TIMEOUT,
114
+ `Advisory recall from provider "${providerName}" timed out after ${effectiveOptions.timeoutMs}ms`,
115
+ );
116
+ const recallPromise = Promise.resolve(
117
+ provider.recall({
118
+ projectPath: path.resolve(target),
119
+ taskId,
120
+ query: normalizedQuery,
121
+ ...effectiveOptions,
122
+ }),
123
+ );
124
+ let timedOut = false;
125
+ const guardedRecallPromise = recallPromise.then(
126
+ (value) => timedOut ? new Promise(() => {}) : value,
127
+ (error) => {
128
+ if (timedOut) return new Promise(() => {});
129
+ throw error;
130
+ },
131
+ );
109
132
  const timeoutPromise = new Promise((_, reject) => {
110
133
  timer = setTimeout(() => {
111
- reject(
112
- serviceError(
113
- E_ADVISORY_CONTEXT_TIMEOUT,
114
- `Advisory recall from provider "${providerName}" timed out after ${effectiveOptions.timeoutMs}ms`,
115
- ),
134
+ timedOut = true;
135
+ cleanupTimer = setTimeout(() => reject(timeoutError), PROVIDER_CLEANUP_GRACE_MS);
136
+ recallPromise.then(
137
+ () => {
138
+ clearTimeout(cleanupTimer);
139
+ reject(timeoutError);
140
+ },
141
+ () => {
142
+ clearTimeout(cleanupTimer);
143
+ reject(timeoutError);
144
+ },
116
145
  );
117
146
  }, effectiveOptions.timeoutMs);
118
147
  });
119
148
 
120
149
  let rawResult;
121
150
  try {
122
- const recallPromise = Promise.resolve(
123
- provider.recall({
124
- projectPath: path.resolve(target),
125
- taskId,
126
- query: normalizedQuery,
127
- ...effectiveOptions,
128
- }),
129
- );
130
- rawResult = await Promise.race([recallPromise, timeoutPromise]);
151
+ rawResult = await Promise.race([guardedRecallPromise, timeoutPromise]);
131
152
  } finally {
132
153
  clearTimeout(timer);
154
+ clearTimeout(cleanupTimer);
133
155
  }
134
156
 
135
157
  return normalizeAdvisoryContextResult(rawResult, {
@@ -1,7 +1,7 @@
1
1
  import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
- import { getActiveTaskTransaction, withTaskTransaction } from "./transaction.js";
4
+ import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
5
5
  import { appendProtocolEvent } from "./events.js";
6
6
  import { canonicalFingerprint } from "./artifacts.js";
7
7
  import {
@@ -49,7 +49,7 @@ async function writeApprovalFile(target, taskId, approval) {
49
49
  const relPath = taskApprovalPath(taskId, approval.approvalId);
50
50
  await assertSafePath(target, relPath);
51
51
  const serialized = `${JSON.stringify(approval, null, 2)}\n`;
52
- const activeTransaction = getActiveTaskTransaction();
52
+ const activeTransaction = (await getTaskTransaction(target));
53
53
  if (activeTransaction) {
54
54
  await activeTransaction.stageText(relPath, serialized);
55
55
  } else {
@@ -5,7 +5,7 @@ import { assertSecretFree } from "./receipt.js";
5
5
  import { assertJsonBytes, assertJsonLimits } from "./json-safety.js";
6
6
  import { assertSchema, readSchema } from "./schema-validation.js";
7
7
  import { getPackageRoot } from "./templates.js";
8
- import { getActiveTaskTransaction, withTaskTransaction } from "./transaction.js";
8
+ import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
9
9
 
10
10
  export const ARTIFACT_PATHS = Object.freeze({
11
11
  contract: ".forgeloop/current-contract.json",
@@ -76,7 +76,7 @@ export async function readJsonArtifact(
76
76
  }
77
77
 
78
78
  const artifactPath = ensureWithin(target, relativePath);
79
- const transaction = getActiveTaskTransaction();
79
+ const transaction = (await getTaskTransaction(target));
80
80
  const stagedText = transaction ? await transaction.readText(relativePath) : null;
81
81
  if (stagedText === null && !(await fileExists(artifactPath))) {
82
82
  throw new ArtifactError(
@@ -124,7 +124,7 @@ export async function writeJsonArtifact(
124
124
  } catch (error) {
125
125
  throw artifactError("ARTIFACT_PATH_INVALID", relativePath, error);
126
126
  }
127
- const activeTransaction = getActiveTaskTransaction();
127
+ const activeTransaction = (await getTaskTransaction(target));
128
128
  if (!activeTransaction && taskId && !dryRun) {
129
129
  return withTaskTransaction({ target, taskId, operation, packageRoot }, async () => (
130
130
  writeJsonArtifact(target, relativePath, value, schemaName, packageRoot, { dryRun, taskId, operation })
@@ -134,36 +134,3 @@ export function assertCheckList(value, label = "checks", options = {}) {
134
134
  });
135
135
  return value;
136
136
  }
137
-
138
- function requiredChecksSatisfiedBy(checks, requiredValues, selector, { allowInferred = false } = {}) {
139
- assertCheckList(checks);
140
- if (!Array.isArray(requiredValues)) throw checkError("E_CHECK_INVALID", "required values must be an array");
141
- const errors = [];
142
- for (const value of requiredValues) {
143
- const candidates = checks.filter((check) => selector(check) === value);
144
- const check = candidates.find((candidate) => candidate.status === "passed"
145
- && (allowInferred || candidate.evidenceKind === "OBSERVED"))
146
- ?? candidates.find((candidate) => candidate.status === "passed")
147
- ?? candidates[0];
148
- if (!check) {
149
- errors.push(checkError("E_EVIDENCE_REQUIRED", `Required check is missing: ${value}`, [value]));
150
- continue;
151
- }
152
- if (check.status !== "passed") {
153
- errors.push(checkError("E_EVIDENCE_REQUIRED", `Required check is not passed: ${value}`, [value]));
154
- continue;
155
- }
156
- if (!allowInferred && check.evidenceKind !== "OBSERVED") {
157
- errors.push(checkError("E_EVIDENCE_KIND_INVALID", `Required check must be observed: ${value}`, [value]));
158
- }
159
- }
160
- return errors;
161
- }
162
-
163
- export function requiredChecksSatisfied(checks, requiredIds, options = {}) {
164
- return requiredChecksSatisfiedBy(checks, requiredIds, (check) => check.id, options);
165
- }
166
-
167
- export function requiredChecksSatisfiedForRequirements(checks, requiredRequirements, options = {}) {
168
- return requiredChecksSatisfiedBy(checks, requiredRequirements, (check) => check.requirement, options);
169
- }
@@ -17,7 +17,7 @@ import { listActions } from "./actions.js";
17
17
  import { readConfig } from "./config.js";
18
18
  import { resolveResponsibilityStatus } from "./responsibility.js";
19
19
  import { createCodeManifest, readCodeManifest, validateCodeManifestBindings, writeCodeManifest } from "./code-manifest.js";
20
- import { getActiveTaskTransaction, withTaskTransaction } from "./transaction.js";
20
+ import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
21
21
  import { validateStructuralQualityCheckProvenance } from "./structural-quality/service.js";
22
22
 
23
23
  async function attestationConfiguration(target, packageRoot, errors) {
@@ -856,7 +856,7 @@ async function runCompleteInternal({
856
856
 
857
857
  export async function runComplete(options = {}) {
858
858
  const taskId = options.taskId ?? null;
859
- if (options.persist !== false && taskId && !getActiveTaskTransaction()) {
859
+ if (options.persist !== false && taskId && !(await getTaskTransaction(options.target))) {
860
860
  return withTaskTransaction({
861
861
  target: options.target,
862
862
  taskId,
@@ -11,7 +11,7 @@ import { PROTOCOL_VERSION } from "./protocol.js";
11
11
  import { isRecoverableCompletionEvidenceCode } from "./completion-recovery.js";
12
12
 
13
13
  import { taskArtifactPath } from "./task-paths.js";
14
- import { getActiveTaskTransaction, withTaskTransaction } from "./transaction.js";
14
+ import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
15
15
 
16
16
  import { assertDiagnosisDetails } from "./diagnosis-model.js";
17
17
  import {
@@ -311,7 +311,7 @@ export function buildProtocolEvent(input, { checkpoint } = {}) {
311
311
  }
312
312
 
313
313
  export async function previewProtocolEvent(target, input, packageRoot, options = {}) {
314
- const activeTransaction = getActiveTaskTransaction();
314
+ const activeTransaction = (await getTaskTransaction(target));
315
315
  if (!activeTransaction) {
316
316
  return withTaskTransaction({
317
317
  target,
@@ -337,7 +337,7 @@ export async function readEvents(target, packageRoot, options = {}) {
337
337
  const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
338
338
  await assertSafePath(target, relPath);
339
339
  const eventsPath = ensureWithin(target, relPath);
340
- const transaction = getActiveTaskTransaction();
340
+ const transaction = (await getTaskTransaction(target));
341
341
  if (transaction) {
342
342
  const stagedText = await transaction.readText(relPath);
343
343
  if (stagedText !== null) return parseEventsText(stagedText, relPath, packageRoot);
@@ -355,7 +355,7 @@ export async function readEventTail(target, packageRoot, options = {}) {
355
355
  const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
356
356
  await assertSafePath(target, relPath);
357
357
  const eventsPath = ensureWithin(target, relPath);
358
- const transaction = getActiveTaskTransaction();
358
+ const transaction = (await getTaskTransaction(target));
359
359
  if (transaction) {
360
360
  const stagedText = await transaction.readText(relPath);
361
361
  if (stagedText !== null) {
@@ -411,7 +411,7 @@ async function parseEventsText(text, relPath, packageRoot) {
411
411
  }
412
412
 
413
413
  export async function appendProtocolEvent(target, input, packageRoot, options = {}) {
414
- const activeTransaction = getActiveTaskTransaction();
414
+ const activeTransaction = (await getTaskTransaction(target));
415
415
  if (typeof input?.taskId !== "string" || !input.taskId) throw protocolError("E_EVENT_INVALID", "event taskId is required");
416
416
  if (typeof input?.event !== "string" || !input.event) throw protocolError("E_EVENT_INVALID", "event type is required");
417
417
  const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
@@ -66,24 +66,37 @@ function contractCollections(contract) {
66
66
 
67
67
  function contractSignals(contract) {
68
68
  const collections = contractCollections(contract);
69
+ const obligations = [];
70
+ const types = new Set();
71
+ function collect(requirement) {
72
+ if (typeof requirement === "string") obligations.push(requirement);
73
+ else if (requirement && typeof requirement === "object") {
74
+ if (typeof requirement.text === "string") obligations.push(requirement.text);
75
+ types.add(requirement.type);
76
+ if (Array.isArray(requirement.requirements)) requirement.requirements.forEach(collect);
77
+ }
78
+ }
79
+ collections.successCriteria.forEach(collect);
80
+ if (Array.isArray(contract?.verification)) contract.verification.forEach(collect);
69
81
  const riskText = collections.risks.join(" ").toLowerCase();
82
+ // Constraints and stop conditions describe boundaries, not obligations.
83
+ // Declared route risks remain authoritative even when a constraint excludes
84
+ // an operation; do not try to interpret prose negation as authorization.
70
85
  const allText = [
71
86
  ...collections.deliverables,
72
- ...collections.successCriteria,
73
- ...collections.constraints,
74
- ...collections.stopConditions,
87
+ ...obligations,
75
88
  ].join(" ").toLowerCase();
76
89
  const combinedText = `${riskText} ${allText}`;
77
90
  const signals = {
78
91
  secrets: /\bsecrets?\b|\bcredentials?\b|\bpasswords?\b|\bprivate keys?\b/.test(combinedText),
79
92
  personalData: /\bpersonal[- ]data\b|\bpii\b|\bsensitive data\b/.test(combinedText),
80
- publication: /\bpublication\b|\bpublish(?:ing|ed)?\b|\bdeploy(?:ment|ed)?\b/.test(combinedText),
93
+ publication: types.has("PUBLICATION") || /\bpublication\b|\bpublish(?:ing|ed)?\b|\bdeploy(?:ment|ed)?\b/.test(combinedText),
81
94
  destructive: /\bdestructive\b|\birreversible\b|\bdrop database\b|\bdelete production\b/.test(combinedText),
82
95
  migration: /\bmigration\b|\bmigrate\b|\bschema change\b|\birreversible persistence\b/.test(combinedText),
83
96
  payment: /\bpayments?\b|\bcheckout\b|\bbilling\b/.test(combinedText),
84
97
  externalMutation: /\bexternal mutation\b|\bmutate(?:s|d)? external\b|\bpublish(?:ing|ed)?\b/.test(combinedText),
85
98
  authoritySensitiveExternalMutation: /\bauthority[- ]sensitive\b|\bhost[- ]authorized external\b|\bexternal mutation requiring authority\b/.test(combinedText),
86
- broadProductionValidation: /\bbroad production validation\b|\bproduction validation\b|\bvalidate in production\b/.test(combinedText),
99
+ broadProductionValidation: types.has("PRODUCTION_READINESS") || /\bbroad production validation\b|\bproduction validation\b|\bvalidate in production\b/.test(combinedText),
87
100
  };
88
101
  return signals;
89
102
  }
@@ -126,7 +126,7 @@ export async function acceptCanonicalHandoff(target, {
126
126
  assertPortableContextSafe(normalizedHarness, { label: "harness" });
127
127
  }
128
128
 
129
- return withTaskTransaction(
129
+ const runAcceptance = () => withTaskTransaction(
130
130
  {
131
131
  target,
132
132
  taskId,
@@ -274,4 +274,13 @@ export async function acceptCanonicalHandoff(target, {
274
274
  };
275
275
  },
276
276
  );
277
+
278
+ for (let attempt = 0; attempt < 3; attempt += 1) {
279
+ try {
280
+ return await runAcceptance();
281
+ } catch (error) {
282
+ if (error?.code !== "E_TASK_LOCKED" || attempt === 2) throw error;
283
+ await new Promise((resolve) => setTimeout(resolve, 25 * (attempt + 1)));
284
+ }
285
+ }
277
286
  }