@cassiomc1/forgeloop 1.9.0 → 1.10.1

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 (103) hide show
  1. package/AGENT_COMPATIBILITY.md +15 -0
  2. package/DELEGATION_PROTOCOL.md +6 -0
  3. package/DOCS_INDEX.md +13 -2
  4. package/LOOP_ENGINEERING.md +17 -4
  5. package/LOOP_SYSTEM_DESIGN.md +56 -0
  6. package/ORCHESTRATOR_INTEGRATION.md +9 -0
  7. package/PROTOCOL_INTEGRATION.md +31 -0
  8. package/QUALITY_SCORECARD.md +2 -2
  9. package/README.md +53 -0
  10. package/TERMINOLOGY.md +12 -0
  11. package/THREAT_MODEL.md +24 -0
  12. package/completions/_forgeloop +2 -1
  13. package/completions/forgeloop.bash +3 -1
  14. package/completions/forgeloop.fish +9 -1
  15. package/docs/ADVISORY_CONTEXT.md +174 -0
  16. package/docs/AGENT_PROTOCOL_SUMMARY.md +28 -2
  17. package/docs/ARTIFACT_REFERENCE.md +29 -0
  18. package/docs/CLI_REFERENCE.md +40 -0
  19. package/docs/CODE_ATTESTATION.md +9 -0
  20. package/docs/CROSS_HARNESS_CONTINUITY.md +85 -0
  21. package/docs/DOCUMENTATION_GUIDE.md +11 -4
  22. package/docs/EXECUTION_PROFILE_BENCHMARKS.md +10 -0
  23. package/docs/GETTING_STARTED.md +22 -0
  24. package/docs/KNOWLEDGE_SOURCES.md +10 -0
  25. package/docs/MCP.md +29 -1
  26. package/docs/PACKAGE_CONTENTS.md +83 -0
  27. package/docs/RECIPES.md +80 -0
  28. package/docs/RELEASE_CHECKLIST.md +22 -0
  29. package/docs/REVISION_PROVIDERS.md +9 -0
  30. package/docs/TROUBLESHOOTING.md +66 -2
  31. package/docs/UNIVERSAL_INTEGRATION.md +60 -0
  32. package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +13 -2
  33. package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +6 -6
  34. package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +10 -1
  35. package/docs/assets/diagrams/forgeloop-engineering-flow.html +22 -11
  36. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  37. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +17 -8
  38. package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +14 -3
  39. package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +6 -6
  40. package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +10 -1
  41. package/docs/diagrams/README.md +17 -0
  42. package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +383 -57
  43. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +374 -55
  44. package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +328 -47
  45. package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
  46. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
  47. package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
  48. package/package.json +14 -4
  49. package/schemas/handoff-envelope.schema.json +1 -0
  50. package/scripts/CI_VALIDATORS.md +24 -0
  51. package/scripts/check-changelog-freshness.mjs +27 -3
  52. package/scripts/check-critical-coverage.mjs +9 -0
  53. package/scripts/generate-agent-protocol-summary.mjs +18 -0
  54. package/src/cli.js +6 -0
  55. package/src/commands/doctor.js +11 -10
  56. package/src/commands/handoff-accept.js +36 -0
  57. package/src/commands/handoff-list.js +28 -2
  58. package/src/commands/handoff-show.js +27 -2
  59. package/src/commands/reconcile-continuity.js +4 -0
  60. package/src/core/actions.js +2 -2
  61. package/src/core/advisory-context/constants.js +74 -0
  62. package/src/core/advisory-context/provider.js +287 -0
  63. package/src/core/advisory-context/service.js +140 -0
  64. package/src/core/approvals.js +2 -2
  65. package/src/core/artifacts.js +3 -3
  66. package/src/core/checks.js +0 -33
  67. package/src/core/cli-command-definitions.js +17 -0
  68. package/src/core/command-executors.js +12 -0
  69. package/src/core/command-input.js +11 -1
  70. package/src/core/completion.js +2 -2
  71. package/src/core/continuity-lint.js +89 -0
  72. package/src/core/continuity-reconciliation.js +16 -0
  73. package/src/core/continuity.js +10 -11
  74. package/src/core/error-codes.js +113 -0
  75. package/src/core/events.js +37 -5
  76. package/src/core/execution-profile-context.js +15 -1
  77. package/src/core/execution-profile.js +18 -5
  78. package/src/core/filesystem.js +18 -2
  79. package/src/core/handoff-acceptance.js +277 -0
  80. package/src/core/handoff.js +41 -8
  81. package/src/core/integration-invocation-policy.js +19 -2
  82. package/src/core/integration-resources.js +21 -1
  83. package/src/core/next-action-pending-actions.js +255 -0
  84. package/src/core/next-action-phases.js +26 -764
  85. package/src/core/next-action-planned-phase.js +51 -0
  86. package/src/core/next-action-quality-guidance.js +19 -0
  87. package/src/core/next-action-recovery-phases.js +97 -0
  88. package/src/core/next-action-refresh.js +20 -0
  89. package/src/core/next-action-review-phase.js +189 -0
  90. package/src/core/next-action-verification-phase.js +192 -0
  91. package/src/core/portable-context.js +103 -0
  92. package/src/core/protocol-info.js +18 -2
  93. package/src/core/runtime-context.js +31 -0
  94. package/src/core/task-recovery.js +2 -2
  95. package/src/core/transaction-maintenance.js +70 -0
  96. package/src/core/transaction.js +31 -10
  97. package/src/core/work-state.js +5 -5
  98. package/src/integration.d.ts +135 -3
  99. package/src/integration.js +23 -0
  100. package/src/core/cli-metadata.js +0 -23
  101. package/src/core/decision-classification.js +0 -55
  102. package/src/core/gates.js +0 -57
  103. package/src/core/workflow-compatibility.js +0 -151
@@ -0,0 +1,140 @@
1
+ import path from "node:path";
2
+
3
+ import {
4
+ normalizePortableText,
5
+ assertPortableContextSafe,
6
+ } from "../portable-context.js";
7
+ import {
8
+ ADVISORY_CONTEXT_LIMITS,
9
+ normalizeAdvisoryRecallOptions,
10
+ } from "./constants.js";
11
+ import {
12
+ normalizeAdvisoryContextResult,
13
+ resolveAdvisoryContextProvider,
14
+ } from "./provider.js";
15
+ import {
16
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
17
+ E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
18
+ E_ADVISORY_CONTEXT_QUERY_INVALID,
19
+ E_ADVISORY_CONTEXT_TIMEOUT,
20
+ E_PORTABLE_CONTEXT_INVALID,
21
+ } from "../error-codes.js";
22
+
23
+ function serviceError(code, message, cause) {
24
+ const error = new Error(message, cause !== undefined ? { cause } : undefined);
25
+ error.name = "AdvisoryContextServiceError";
26
+ error.code = code;
27
+ return error;
28
+ }
29
+
30
+ export async function recallAdvisoryContext({
31
+ target,
32
+ taskId,
33
+ providerName,
34
+ query,
35
+ limit,
36
+ maxItemChars,
37
+ maxTotalChars,
38
+ timeoutMs,
39
+ runtimeContext,
40
+ } = {}) {
41
+ if (!target || typeof target !== "string") {
42
+ throw serviceError(
43
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
44
+ "Target project path is required for advisory recall",
45
+ );
46
+ }
47
+
48
+ if (!taskId || typeof taskId !== "string") {
49
+ throw serviceError(
50
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
51
+ "taskId is required for advisory recall",
52
+ );
53
+ }
54
+
55
+ if (!providerName || typeof providerName !== "string") {
56
+ throw serviceError(
57
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
58
+ "providerName is required for advisory recall",
59
+ );
60
+ }
61
+
62
+ let normalizedQuery;
63
+ try {
64
+ normalizedQuery = normalizePortableText(query, {
65
+ label: "advisory query",
66
+ maxLength: ADVISORY_CONTEXT_LIMITS.maxQueryChars,
67
+ });
68
+ assertPortableContextSafe(normalizedQuery, { label: "advisory query" });
69
+ } catch (err) {
70
+ if (err.code === E_PORTABLE_CONTEXT_INVALID) {
71
+ throw err;
72
+ }
73
+ throw serviceError(
74
+ E_ADVISORY_CONTEXT_QUERY_INVALID,
75
+ `Advisory context query is invalid: ${err.message}`,
76
+ err,
77
+ );
78
+ }
79
+
80
+ const effectiveOptions = normalizeAdvisoryRecallOptions({
81
+ limit,
82
+ maxItemChars,
83
+ maxTotalChars,
84
+ timeoutMs,
85
+ });
86
+
87
+ let provider;
88
+ try {
89
+ provider = await resolveAdvisoryContextProvider({
90
+ providers: runtimeContext?.advisoryContextProviders,
91
+ providerName,
92
+ });
93
+ } catch (err) {
94
+ throw serviceError(
95
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
96
+ `Advisory context provider "${providerName}" failed validation: ${err.message}`,
97
+ err,
98
+ );
99
+ }
100
+
101
+ if (!provider) {
102
+ throw serviceError(
103
+ E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
104
+ `Advisory context provider "${providerName}" is not registered in runtime context`,
105
+ );
106
+ }
107
+
108
+ let timer;
109
+ const timeoutPromise = new Promise((_, reject) => {
110
+ timer = setTimeout(() => {
111
+ reject(
112
+ serviceError(
113
+ E_ADVISORY_CONTEXT_TIMEOUT,
114
+ `Advisory recall from provider "${providerName}" timed out after ${effectiveOptions.timeoutMs}ms`,
115
+ ),
116
+ );
117
+ }, effectiveOptions.timeoutMs);
118
+ });
119
+
120
+ let rawResult;
121
+ 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]);
131
+ } finally {
132
+ clearTimeout(timer);
133
+ }
134
+
135
+ return normalizeAdvisoryContextResult(rawResult, {
136
+ provider,
137
+ taskId,
138
+ ...effectiveOptions,
139
+ });
140
+ }
@@ -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
- }
@@ -1172,6 +1172,23 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
1172
1172
  mayExecuteExternalProcess: false,
1173
1173
  description: "Reads and verifies one immutable handoff snapshot.",
1174
1174
  }),
1175
+ "handoff-accept": Object.freeze({
1176
+ name: "handoff-accept",
1177
+ category: "task",
1178
+ mutation: "MUTATING",
1179
+ options: Object.freeze({
1180
+ ...CLI_COMMON_OPTIONS,
1181
+ ...CLI_TASK_OPTION,
1182
+ "--handoff": Object.freeze({ targetKey: "handoffId", parseType: "string", takesValue: true, valueName: "id", missingValueMessage: "--handoff requires a handoff ID", description: "handoff identifier" }),
1183
+ "--consumer-id": Object.freeze({ targetKey: "consumerId", parseType: "string", takesValue: true, valueName: "id", missingValueMessage: "--consumer-id requires a consumer ID", description: "consumer identifier accepting the handoff" }),
1184
+ "--harness": Object.freeze({ targetKey: "harness", parseType: "string", takesValue: true, valueName: "name", missingValueMessage: "--harness requires a harness name", description: "optional harness accepting the handoff" }),
1185
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit acceptance result as JSON" }),
1186
+ }),
1187
+ writes: [".forgeloop/task-state/<taskKey>/events.ndjson"],
1188
+ removes: [],
1189
+ mayExecuteExternalProcess: false,
1190
+ description: "Accepts an immutable handoff exactly once in the task event ledger.",
1191
+ }),
1175
1192
  "responsibility-set": Object.freeze({
1176
1193
  name: "responsibility-set",
1177
1194
  category: "scope",
@@ -73,6 +73,7 @@ import { runWorkspaceStatus } from "../commands/workspace-status.js";
73
73
  import { runHandoffCreate } from "../commands/handoff-create.js";
74
74
  import { runHandoffList } from "../commands/handoff-list.js";
75
75
  import { runHandoffShow } from "../commands/handoff-show.js";
76
+ import { runHandoffAccept } from "../commands/handoff-accept.js";
76
77
  import { runResponsibilitySet } from "../commands/responsibility-set.js";
77
78
  import { runResponsibilityStatus } from "../commands/responsibility-status.js";
78
79
  import { runVerifyScope } from "../commands/verify-scope.js";
@@ -236,6 +237,17 @@ export const COMMAND_EXECUTORS = {
236
237
  result: await runHandoffShow({ target, packageRoot, taskId: options.taskId, handoffId: options.handoffId }),
237
238
  exitCode: 0,
238
239
  }),
240
+ "handoff-accept": async ({ target, packageRoot, options }) => ({
241
+ result: await runHandoffAccept({
242
+ target,
243
+ packageRoot,
244
+ taskId: options.taskId,
245
+ handoffId: options.handoffId,
246
+ consumerId: options.consumerId,
247
+ harness: options.harness,
248
+ }),
249
+ exitCode: 0,
250
+ }),
239
251
  "responsibility-set": async ({ target, packageRoot, options }) => ({
240
252
  result: await runResponsibilitySet({
241
253
  target,
@@ -70,6 +70,8 @@ export function defaultCommandInputValues() {
70
70
  recipientHint: null,
71
71
  handoffNote: null,
72
72
  handoffId: null,
73
+ consumerId: null,
74
+ harness: null,
73
75
  responsibilityLabel: null,
74
76
  responsibilityAllowedPaths: [],
75
77
  responsibilityReadOnlyPaths: [],
@@ -148,13 +150,21 @@ export function validateForgeLoopCommandInput({ command, input, help = false } =
148
150
  if (options.compact === true && !["next", "task-show"].includes(command)) {
149
151
  throw inputError(`compact output is not valid for ${command}`);
150
152
  }
151
- if (["workspace-bind", "workspace-status", "handoff-create", "handoff-list", "handoff-show", "responsibility-set", "responsibility-status", "verify-scope", "attestation-create", "attestation-status", "attestation-verify"].includes(command)
153
+ if (["workspace-bind", "workspace-status", "handoff-create", "handoff-list", "handoff-show", "handoff-accept", "responsibility-set", "responsibility-status", "verify-scope", "attestation-create", "attestation-status", "attestation-verify"].includes(command)
152
154
  && !options.taskId) {
153
155
  throw inputError(`${command} requires --task`);
154
156
  }
155
157
  if (command === "handoff-show" && !help && !options.handoffId) {
156
158
  throw inputError("handoff-show requires --id");
157
159
  }
160
+ if (command === "handoff-accept" && !help) {
161
+ if (!options.handoffId) {
162
+ throw inputError("handoff-accept requires --handoff");
163
+ }
164
+ if (!options.consumerId) {
165
+ throw inputError("handoff-accept requires --consumer-id");
166
+ }
167
+ }
158
168
  if (command === "responsibility-set" && !help && !options.responsibilityLabel) {
159
169
  throw inputError("responsibility-set requires --label");
160
170
  }
@@ -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,
@@ -0,0 +1,89 @@
1
+ import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
2
+
3
+ function finding(code, severity, field, itemId = null) {
4
+ return { code, severity, field, itemId };
5
+ }
6
+
7
+ function hasOperationalHints(continuity) {
8
+ return Boolean(
9
+ continuity.currentFocus
10
+ || continuity.remainingWork?.length
11
+ || continuity.knownIssues?.length
12
+ || continuity.changedAreas?.length
13
+ || continuity.inspectFirst?.length
14
+ || (typeof continuity.resumeNote === "string" && continuity.resumeNote.trim() !== ""),
15
+ );
16
+ }
17
+
18
+ export async function lintContinuity({ target, continuity, state } = {}) {
19
+ const findings = [];
20
+ if (!continuity || typeof continuity !== "object" || Array.isArray(continuity)) {
21
+ return { status: "PASS", findings };
22
+ }
23
+
24
+ const completedSteps = new Set(
25
+ Array.isArray(state?.completedSteps) ? state.completedSteps : [],
26
+ );
27
+
28
+ for (const [index, item] of (continuity.remainingWork ?? []).entries()) {
29
+ if (!item?.id) continue;
30
+ if (completedSteps.has(item.id)) {
31
+ findings.push(finding(
32
+ "CONTINUITY_REMAINING_ALREADY_COMPLETED",
33
+ "WARN",
34
+ `remainingWork[${index}]`,
35
+ item.id,
36
+ ));
37
+ }
38
+ }
39
+
40
+ if (continuity.currentFocus?.id && completedSteps.has(continuity.currentFocus.id)) {
41
+ findings.push(finding(
42
+ "CONTINUITY_FOCUS_ALREADY_COMPLETED",
43
+ "WARN",
44
+ "currentFocus",
45
+ continuity.currentFocus.id,
46
+ ));
47
+ }
48
+
49
+ const knownIssueIds = new Set(
50
+ (continuity.knownIssues ?? []).map((item) => item?.id).filter(Boolean),
51
+ );
52
+ for (const [index, item] of (continuity.remainingWork ?? []).entries()) {
53
+ if (item?.id && knownIssueIds.has(item.id)) {
54
+ findings.push(finding(
55
+ "CONTINUITY_ITEM_ROLE_CONFLICT",
56
+ "WARN",
57
+ `remainingWork[${index}]`,
58
+ item.id,
59
+ ));
60
+ }
61
+ }
62
+
63
+ if (typeof target === "string" && target.trim() !== "") {
64
+ for (const [index, inspectPath] of (continuity.inspectFirst ?? []).entries()) {
65
+ try {
66
+ await assertSafePath(target, inspectPath);
67
+ if (!(await fileExists(ensureWithin(target, inspectPath)))) {
68
+ findings.push(finding(
69
+ "CONTINUITY_INSPECT_PATH_MISSING",
70
+ "WARN",
71
+ `inspectFirst[${index}]`,
72
+ ));
73
+ }
74
+ } catch {
75
+ // Invalid or unsafe paths are rejected by continuity schema validation;
76
+ // lint never resolves an unchecked path or turns it into authority.
77
+ }
78
+ }
79
+ }
80
+
81
+ if (!hasOperationalHints(continuity)) {
82
+ findings.push(finding("CONTINUITY_EMPTY_HINT_SET", "INFO", "continuity"));
83
+ }
84
+
85
+ return {
86
+ status: findings.some((item) => item.severity === "WARN") ? "WARN" : "PASS",
87
+ findings,
88
+ };
89
+ }
@@ -1,6 +1,7 @@
1
1
  import { canonicalFingerprint } from "./artifacts.js";
2
2
  import { assertContinuitySemantics, readContinuity } from "./continuity.js";
3
3
  import { WORK_TRANSITIONS } from "./protocol.js";
4
+ import { lintContinuity } from "./continuity-lint.js";
4
5
 
5
6
  const RECONCILIATION_CODE = "E_CONTINUITY_RECONCILIATION_REQUIRED";
6
7
 
@@ -188,6 +189,7 @@ export async function reconcileContinuity({ target, packageRoot, taskId = null }
188
189
  path: ".forgeloop/continuity.json",
189
190
  present: false,
190
191
  latestHandoff,
192
+ lint: { status: "PASS", findings: [] },
191
193
  diagnosticContext: await deriveDiagnosticContextSafe({ target, packageRoot, state }),
192
194
  };
193
195
  }
@@ -200,6 +202,15 @@ export async function reconcileContinuity({ target, packageRoot, taskId = null }
200
202
  present: true,
201
203
  error: error.message,
202
204
  latestHandoff,
205
+ lint: {
206
+ status: "WARN",
207
+ findings: [{
208
+ code: error.code ?? "CONTINUITY_INVALID",
209
+ severity: "WARN",
210
+ field: "continuity",
211
+ itemId: null,
212
+ }],
213
+ },
203
214
  };
204
215
  }
205
216
 
@@ -229,6 +240,11 @@ export async function reconcileContinuity({ target, packageRoot, taskId = null }
229
240
  fingerprint: continuityArtifact.fingerprint,
230
241
  continuity: continuityArtifact.value,
231
242
  latestHandoff,
243
+ lint: await lintContinuity({
244
+ target,
245
+ continuity: continuityArtifact.value,
246
+ state,
247
+ }),
232
248
  diagnosticContext: await deriveDiagnosticContextSafe({ target, packageRoot, state }),
233
249
  };
234
250
  }
@@ -9,7 +9,7 @@ import {
9
9
  } from "./artifacts.js";
10
10
  import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
11
11
  import { PROTOCOL_VERSION, WORK_PHASES } from "./protocol.js";
12
- import { assertSecretFree } from "./receipt.js";
12
+ import { normalizePortableText, assertPortableContextSafe } from "./portable-context.js";
13
13
  import { getPackageRoot } from "./templates.js";
14
14
  import { taskArtifactPath } from "./task-paths.js";
15
15
 
@@ -37,16 +37,11 @@ function continuityError(code, message, artifacts = [CONTINUITY_PATH]) {
37
37
  }
38
38
 
39
39
  function nonEmptyString(value, label, maxLength) {
40
- if (typeof value !== "string" || value.trim() === "") {
41
- throw continuityError("E_CONTINUITY_INVALID", `${label} must be a non-empty string`);
40
+ try {
41
+ return normalizePortableText(value, { label, maxLength });
42
+ } catch (error) {
43
+ throw continuityError("E_CONTINUITY_INVALID", error.message);
42
44
  }
43
- if (value.length > maxLength) {
44
- throw continuityError("E_CONTINUITY_INVALID", `${label} exceeds the ${maxLength}-character limit`);
45
- }
46
- if (/\p{Cc}/u.test(value)) {
47
- throw continuityError("E_CONTINUITY_INVALID", `${label} contains control characters`);
48
- }
49
- return value;
50
45
  }
51
46
 
52
47
  function fingerprint(value, label) {
@@ -171,7 +166,11 @@ export function assertContinuitySemantics(input) {
171
166
  ? { resumeNote: nonEmptyString(input.resumeNote, "resumeNote", LIMITS.resumeNote) }
172
167
  : {}),
173
168
  };
174
- assertSecretFree(normalized);
169
+ try {
170
+ assertPortableContextSafe(normalized);
171
+ } catch (error) {
172
+ throw continuityError("E_CONTINUITY_INVALID", error.message);
173
+ }
175
174
  return normalized;
176
175
  }
177
176
 
@@ -105,6 +105,19 @@ export const E_STRUCTURAL_QUALITY_OBSERVATION_EPOCH_STALE = "E_STRUCTURAL_QUALIT
105
105
  export const E_STRUCTURAL_QUALITY_PROJECTION_INCOMPLETE = "E_STRUCTURAL_QUALITY_PROJECTION_INCOMPLETE";
106
106
  export const E_STRUCTURAL_QUALITY_REGRESSION = "E_STRUCTURAL_QUALITY_REGRESSION";
107
107
 
108
+ export const E_ADVISORY_CONTEXT_PROVIDER_INVALID = "E_ADVISORY_CONTEXT_PROVIDER_INVALID";
109
+ export const E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE = "E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE";
110
+ export const E_ADVISORY_CONTEXT_QUERY_INVALID = "E_ADVISORY_CONTEXT_QUERY_INVALID";
111
+ export const E_ADVISORY_CONTEXT_REQUEST_INVALID = "E_ADVISORY_CONTEXT_REQUEST_INVALID";
112
+ export const E_ADVISORY_CONTEXT_RESULT_INVALID = "E_ADVISORY_CONTEXT_RESULT_INVALID";
113
+ export const E_ADVISORY_CONTEXT_TIMEOUT = "E_ADVISORY_CONTEXT_TIMEOUT";
114
+ export const E_ADVISORY_CONTEXT_OUTPUT_LIMIT = "E_ADVISORY_CONTEXT_OUTPUT_LIMIT";
115
+ export const E_PORTABLE_CONTEXT_INVALID = "E_PORTABLE_CONTEXT_INVALID";
116
+ export const E_HANDOFF_ACCEPTANCE_UNBOUND = "E_HANDOFF_ACCEPTANCE_UNBOUND";
117
+ export const E_HANDOFF_STALE = "E_HANDOFF_STALE";
118
+ export const E_HANDOFF_ALREADY_ACCEPTED = "E_HANDOFF_ALREADY_ACCEPTED";
119
+ export const E_HANDOFF_ACCEPTANCE_INCONSISTENT = "E_HANDOFF_ACCEPTANCE_INCONSISTENT";
120
+
108
121
  const STRUCTURAL_QUALITY_ERROR_METADATA = Object.freeze(Object.fromEntries([
109
122
  [E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID, "Correct structuralQuality mode, provider ID, budgets, floors, or optimization limits in .forgeloop/config.json."],
110
123
  [E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "Use a provider implementing id, detect(input), and scan(input) with the documented normalized boundary."],
@@ -273,12 +286,100 @@ export const E_APPROVAL_ALREADY_RESOLVED = "E_APPROVAL_ALREADY_RESOLVED";
273
286
  export const E_TRAJECTORY_SCENARIO_INVALID = "E_TRAJECTORY_SCENARIO_INVALID";
274
287
  export const E_TRAJECTORY_REFERENCE_REQUIRED = "E_TRAJECTORY_REFERENCE_REQUIRED";
275
288
 
289
+ const ADVISORY_CONTEXT_AND_HANDOFF_ERROR_METADATA = Object.freeze(Object.fromEntries([
290
+ [E_ADVISORY_CONTEXT_PROVIDER_INVALID, Object.freeze({
291
+ code: E_ADVISORY_CONTEXT_PROVIDER_INVALID,
292
+ category: "advisory-context",
293
+ classification: "PUBLIC_STABLE",
294
+ meaning: "Advisory context provider configuration or interface implementation is invalid.",
295
+ safeResolution: "Use a provider implementing id, recall(input) with bounded query parameters; advisory context is optional.",
296
+ })],
297
+ [E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE, Object.freeze({
298
+ code: E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
299
+ category: "advisory-context",
300
+ classification: "PUBLIC_STABLE",
301
+ meaning: "Requested advisory context provider is not registered in runtime context.",
302
+ safeResolution: "Register the provider in runtime context before recall, or proceed without advisory context; provider failure never blocks canonical lifecycle.",
303
+ })],
304
+ [E_ADVISORY_CONTEXT_QUERY_INVALID, Object.freeze({
305
+ code: E_ADVISORY_CONTEXT_QUERY_INVALID,
306
+ category: "advisory-context",
307
+ classification: "PUBLIC_STABLE",
308
+ meaning: "Advisory context query failed portable-context validation or exceeded budget.",
309
+ safeResolution: "Provide a bounded query free of control characters and secret-like values.",
310
+ })],
311
+ [E_ADVISORY_CONTEXT_REQUEST_INVALID, Object.freeze({
312
+ code: E_ADVISORY_CONTEXT_REQUEST_INVALID,
313
+ category: "advisory-context",
314
+ classification: "PUBLIC_STABLE",
315
+ meaning: "Advisory context recall budgets are not finite integer values within the supported request contract.",
316
+ safeResolution: "Provide finite integer limit, maxItemChars, maxTotalChars, and timeoutMs values; oversized valid values are clamped to documented maxima.",
317
+ })],
318
+ [E_ADVISORY_CONTEXT_RESULT_INVALID, Object.freeze({
319
+ code: E_ADVISORY_CONTEXT_RESULT_INVALID,
320
+ category: "advisory-context",
321
+ classification: "PUBLIC_STABLE",
322
+ meaning: "Advisory context provider returned an invalid result structure.",
323
+ safeResolution: "Ensure provider returns items with string summary and optional title, sourceRef, observedAt, confidence.",
324
+ })],
325
+ [E_ADVISORY_CONTEXT_TIMEOUT, Object.freeze({
326
+ code: E_ADVISORY_CONTEXT_TIMEOUT,
327
+ category: "advisory-context",
328
+ classification: "PUBLIC_STABLE",
329
+ meaning: "Advisory context recall exceeded its execution timeout.",
330
+ safeResolution: "Use a responsive provider or increase timeout within limits; advisory context is optional.",
331
+ })],
332
+ [E_ADVISORY_CONTEXT_OUTPUT_LIMIT, Object.freeze({
333
+ code: E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
334
+ category: "advisory-context",
335
+ classification: "PUBLIC_STABLE",
336
+ meaning: "Advisory context output exceeded the configured character or item limit.",
337
+ safeResolution: "Reduce query scope, limit items, or truncate oversized summaries at the provider.",
338
+ })],
339
+ [E_PORTABLE_CONTEXT_INVALID, Object.freeze({
340
+ code: E_PORTABLE_CONTEXT_INVALID,
341
+ category: "portable-context",
342
+ classification: "PUBLIC_STABLE",
343
+ meaning: "Text or object failed portable-context safety, character, or secret limits.",
344
+ safeResolution: "Ensure text is bounded, contains no control characters, and contains no secret-like values.",
345
+ })],
346
+ [E_HANDOFF_ACCEPTANCE_UNBOUND, Object.freeze({
347
+ code: E_HANDOFF_ACCEPTANCE_UNBOUND,
348
+ category: "handoff",
349
+ classification: "PUBLIC_STABLE",
350
+ meaning: "Handoff snapshot lacks required workStateFingerprint binding.",
351
+ safeResolution: "Create a fresh handoff from the current ForgeLoop version before accepting it.",
352
+ })],
353
+ [E_HANDOFF_STALE, Object.freeze({
354
+ code: E_HANDOFF_STALE,
355
+ category: "handoff",
356
+ classification: "PUBLIC_STABLE",
357
+ meaning: "Handoff snapshot has drifted from the current canonical task state or repository.",
358
+ safeResolution: "Create a new fresh handoff from the current task state instead of accepting a stale snapshot.",
359
+ })],
360
+ [E_HANDOFF_ALREADY_ACCEPTED, Object.freeze({
361
+ code: E_HANDOFF_ALREADY_ACCEPTED,
362
+ category: "handoff",
363
+ classification: "PUBLIC_STABLE",
364
+ meaning: "Handoff was already accepted by a different consumer.",
365
+ safeResolution: "Create a new handoff for the new consumer; do not manually edit acceptance events.",
366
+ })],
367
+ [E_HANDOFF_ACCEPTANCE_INCONSISTENT, Object.freeze({
368
+ code: E_HANDOFF_ACCEPTANCE_INCONSISTENT,
369
+ category: "handoff",
370
+ classification: "PUBLIC_STABLE",
371
+ meaning: "Handoff acceptance disagrees with task event ledger history.",
372
+ safeResolution: "Verify ledger integrity and require a preceding valid HANDOFF_CREATED event.",
373
+ })],
374
+ ]));
375
+
276
376
  /**
277
377
  * Public, stable ForgeLoop error and reason codes documented for users and harnesses.
278
378
  */
279
379
  export const PUBLIC_ERROR_CODES = Object.freeze({
280
380
  ...EXTENSION_PUBLIC_ERROR_CODES,
281
381
  ...STRUCTURAL_QUALITY_ERROR_METADATA,
382
+ ...ADVISORY_CONTEXT_AND_HANDOFF_ERROR_METADATA,
282
383
  E_PREFLIGHT_NOT_READY: Object.freeze({
283
384
  code: "E_PREFLIGHT_NOT_READY",
284
385
  category: "preflight",
@@ -1205,6 +1306,18 @@ export const ALL_KNOWN_ERROR_CODES = Object.freeze(new Set([
1205
1306
  E_FAILURE_SIGNATURE_INVALID,
1206
1307
  E_STRATEGY_OSCILLATION,
1207
1308
  "E_TRACE_SNAPSHOT_INCONSISTENT",
1309
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
1310
+ E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
1311
+ E_ADVISORY_CONTEXT_QUERY_INVALID,
1312
+ E_ADVISORY_CONTEXT_REQUEST_INVALID,
1313
+ E_ADVISORY_CONTEXT_RESULT_INVALID,
1314
+ E_ADVISORY_CONTEXT_TIMEOUT,
1315
+ E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
1316
+ E_PORTABLE_CONTEXT_INVALID,
1317
+ E_HANDOFF_ACCEPTANCE_UNBOUND,
1318
+ E_HANDOFF_STALE,
1319
+ E_HANDOFF_ALREADY_ACCEPTED,
1320
+ E_HANDOFF_ACCEPTANCE_INCONSISTENT,
1208
1321
  ]));
1209
1322
 
1210
1323
  /**