@cassiomc1/forgeloop 1.3.0 → 1.6.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.
Files changed (163) hide show
  1. package/.github/copilot-instructions.md +1 -0
  2. package/AGENTS.md +1 -0
  3. package/CLAUDE.md +1 -0
  4. package/DOCS_INDEX.md +20 -8
  5. package/EXECUTION_STATE.md +60 -0
  6. package/LOOP_ENGINEERING.md +135 -5
  7. package/LOOP_SYSTEM_DESIGN.md +54 -1
  8. package/PROTOCOL_INTEGRATION.md +87 -0
  9. package/QUALITY_SCORECARD.md +2 -0
  10. package/README.md +69 -9
  11. package/TERMINOLOGY.md +15 -0
  12. package/THIRD_PARTY_NOTICES.md +30 -0
  13. package/THREAT_MODEL.md +59 -1
  14. package/docs/ARTIFACT_REFERENCE.md +183 -0
  15. package/docs/CLI_REFERENCE.md +391 -6
  16. package/docs/CROSS_HARNESS_CONTINUITY.md +23 -0
  17. package/docs/DIAGNOSTIC_MODEL.md +181 -0
  18. package/docs/DOCUMENTATION_GUIDE.md +36 -13
  19. package/docs/EXECUTION_TRACE.md +76 -0
  20. package/docs/GETTING_STARTED.md +1 -0
  21. package/docs/MCP.md +159 -0
  22. package/docs/RECIPES.md +149 -0
  23. package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
  24. package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
  25. package/docs/TROUBLESHOOTING.md +217 -3
  26. package/docs/UNIVERSAL_INTEGRATION.md +48 -0
  27. package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
  28. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
  29. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
  30. package/docs/diagrams/README.md +55 -0
  31. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
  32. package/docs/diagrams/manifest.json +42 -0
  33. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
  34. package/package.json +21 -8
  35. package/schemas/action.schema.json +100 -0
  36. package/schemas/approval.schema.json +51 -0
  37. package/schemas/capability-policy.schema.json +41 -0
  38. package/schemas/diagnostic-case.schema.json +85 -0
  39. package/schemas/execution-receipt.schema.json +16 -0
  40. package/schemas/hypothesis-disposition.schema.json +16 -0
  41. package/schemas/intervention.schema.json +27 -0
  42. package/schemas/policy-lock.schema.json +1 -0
  43. package/schemas/policy-snapshot.schema.json +2 -0
  44. package/schemas/task-recovery.schema.json +61 -0
  45. package/schemas/trajectory-evaluation.schema.json +64 -0
  46. package/schemas/trajectory-scenario.schema.json +42 -0
  47. package/src/cli.js +267 -347
  48. package/src/commands/action-authorize.js +41 -0
  49. package/src/commands/action-propose.js +10 -0
  50. package/src/commands/action-reconcile.js +10 -0
  51. package/src/commands/action-record.js +47 -0
  52. package/src/commands/action-show.js +10 -0
  53. package/src/commands/action-verify.js +10 -0
  54. package/src/commands/advance.js +7 -2
  55. package/src/commands/approval-request.js +64 -0
  56. package/src/commands/approval-resolve.js +10 -0
  57. package/src/commands/audit.js +5 -0
  58. package/src/commands/baseline.js +3 -3
  59. package/src/commands/eval.js +6 -0
  60. package/src/commands/history.js +18 -0
  61. package/src/commands/init.js +2 -2
  62. package/src/commands/inspect.js +55 -0
  63. package/src/commands/metrics.js +7 -0
  64. package/src/commands/next.js +8 -2
  65. package/src/commands/policy-discover.js +2 -2
  66. package/src/commands/progress.js +6 -2
  67. package/src/commands/record-diagnosis.js +37 -1
  68. package/src/commands/record-hypothesis-disposition.js +45 -0
  69. package/src/commands/record-intervention.js +35 -0
  70. package/src/commands/reflect.js +38 -0
  71. package/src/commands/report.js +9 -1
  72. package/src/commands/run-action.js +18 -0
  73. package/src/commands/status.js +17 -0
  74. package/src/commands/task-create.js +39 -1
  75. package/src/commands/task-list.js +14 -1
  76. package/src/commands/task-lock-status.js +2 -2
  77. package/src/commands/task-recover.js +202 -0
  78. package/src/commands/task-repair-legacy-recovery.js +417 -0
  79. package/src/commands/task-resume.js +172 -0
  80. package/src/commands/task-scope.js +23 -4
  81. package/src/commands/task-show.js +18 -4
  82. package/src/commands/trace.js +34 -0
  83. package/src/commands/validate-protocol.js +40 -15
  84. package/src/core/action-authorization.js +106 -0
  85. package/src/core/action-constants.js +86 -0
  86. package/src/core/action-execution.js +105 -0
  87. package/src/core/action-ledger-projection.js +302 -0
  88. package/src/core/action-model.js +581 -0
  89. package/src/core/action-readiness.js +141 -0
  90. package/src/core/action-reconciliation-policy.js +49 -0
  91. package/src/core/action-reconciliation.js +66 -0
  92. package/src/core/action-verification.js +111 -0
  93. package/src/core/actions.js +462 -0
  94. package/src/core/approvals.js +405 -0
  95. package/src/core/artifact-registry.js +60 -0
  96. package/src/core/audit.js +45 -4
  97. package/src/core/bundles.js +30 -0
  98. package/src/core/capability-policy.js +226 -0
  99. package/src/core/cli-command-definitions.js +260 -5
  100. package/src/core/command-executors.js +543 -0
  101. package/src/core/command-input.js +107 -0
  102. package/src/core/command-runtime.js +117 -0
  103. package/src/core/completion-artifacts.js +39 -15
  104. package/src/core/completion-ownership.js +88 -0
  105. package/src/core/completion-recovery-rebind.js +194 -0
  106. package/src/core/completion.js +70 -0
  107. package/src/core/continuity-reconciliation.js +24 -5
  108. package/src/core/diagnostic-model.js +396 -0
  109. package/src/core/diagnostic-projection.js +51 -0
  110. package/src/core/diagnostic-record.js +360 -0
  111. package/src/core/error-codes.js +461 -1
  112. package/src/core/events.js +171 -2
  113. package/src/core/execution-prerequisites.js +4 -1
  114. package/src/core/execution.js +26 -188
  115. package/src/core/failure-signature.js +70 -0
  116. package/src/core/failure-surface.js +57 -0
  117. package/src/core/filesystem.js +55 -6
  118. package/src/core/history.js +110 -0
  119. package/src/core/hypothesis-projection.js +85 -0
  120. package/src/core/information-gain-projection.js +283 -0
  121. package/src/core/information-gain.js +138 -0
  122. package/src/core/inspect.js +132 -7
  123. package/src/core/integration-invocation-policy.js +217 -0
  124. package/src/core/integration-limits.js +20 -0
  125. package/src/core/integration-resources.js +178 -0
  126. package/src/core/next-action-model.js +94 -0
  127. package/src/core/next-action.js +490 -3
  128. package/src/core/phase.js +42 -22
  129. package/src/core/policy-engine.js +113 -6
  130. package/src/core/preflight-consistency.js +31 -5
  131. package/src/core/preflight.js +19 -2
  132. package/src/core/prepared-execution.js +227 -0
  133. package/src/core/progress.js +41 -4
  134. package/src/core/project-root.js +21 -0
  135. package/src/core/protocol-info.js +61 -0
  136. package/src/core/protocol.js +14 -0
  137. package/src/core/receipt.js +1 -0
  138. package/src/core/reconcile-closure.js +35 -10
  139. package/src/core/recovery-history.js +116 -0
  140. package/src/core/reflection.js +305 -0
  141. package/src/core/resumability.js +57 -3
  142. package/src/core/schema-validation.js +9 -0
  143. package/src/core/strategy-analysis.js +97 -0
  144. package/src/core/task-claim-state.js +272 -0
  145. package/src/core/task-command.js +5 -1
  146. package/src/core/task-conflict-inspection.js +321 -0
  147. package/src/core/task-context.js +32 -29
  148. package/src/core/task-discovery.js +14 -1
  149. package/src/core/task-lock.js +216 -22
  150. package/src/core/task-paths.js +31 -2
  151. package/src/core/task-recovery-migration.js +192 -0
  152. package/src/core/task-recovery.js +205 -0
  153. package/src/core/task-scope.js +33 -1
  154. package/src/core/task-snapshot.js +53 -0
  155. package/src/core/templates.js +9 -0
  156. package/src/core/trace.js +548 -0
  157. package/src/core/trajectory-evaluation.js +71 -0
  158. package/src/core/trajectory-metrics.js +80 -0
  159. package/src/core/transaction.js +36 -2
  160. package/src/core/work-state.js +10 -5
  161. package/src/integration.js +47 -0
  162. package/docs/assets/forgeloop-flow.svg +0 -1
  163. package/docs/forgeloop-flow.mmd +0 -51
@@ -0,0 +1,138 @@
1
+ export const GAIN_DIMENSIONS = Object.freeze([
2
+ "newObservation",
3
+ "newContributor",
4
+ "newHypothesis",
5
+ "hypothesisDispositionChanged",
6
+ "failureSignatureChanged",
7
+ "failureSurfaceChanged",
8
+ "interventionChanged",
9
+ "strategyChanged",
10
+ "newEvidence",
11
+ "hypothesisEliminated",
12
+ ]);
13
+
14
+ function statementSet(values) {
15
+ return new Set((values ?? []).map((value) => `${value}`.trim().toLowerCase()).filter(Boolean));
16
+ }
17
+
18
+ export function normalizeDiagnosticSnapshot(input) {
19
+ // Idempotent: already-normalized snapshots pass through unchanged so
20
+ // repeated normalization never fabricates empty semantic sets.
21
+ if (input
22
+ && input.observationStatements instanceof Set
23
+ && input.contributorStatements instanceof Set
24
+ && input.hypothesisStatements instanceof Set
25
+ && input.evidenceRefs instanceof Set) {
26
+ return input;
27
+ }
28
+ if (!input) {
29
+ return {
30
+ observationStatements: new Set(),
31
+ contributorStatements: new Set(),
32
+ hypothesisStatements: new Set(),
33
+ evidenceRefs: new Set(),
34
+ };
35
+ }
36
+ const legacyHypothesis = input.legacy
37
+ ? [input.hypothesis ?? ""]
38
+ : (input.hypotheses ?? []).map((hypothesis) => hypothesis.statement ?? "");
39
+ const evidenceRefs = input.legacy
40
+ ? (input.evidenceRefs ?? [])
41
+ : [
42
+ ...(input.hypotheses ?? []).flatMap((hypothesis) => hypothesis.evidenceRefs ?? []),
43
+ ...(input.observations ?? []).map((observation) => observation.evidenceRef).filter(Boolean),
44
+ ];
45
+ return {
46
+ observationStatements: statementSet(input.legacy ? [] : (input.observations ?? []).map((observation) => observation.statement)),
47
+ contributorStatements: statementSet(input.legacy ? [] : (input.contributors ?? []).map((contributor) => contributor.statement)),
48
+ hypothesisStatements: statementSet(legacyHypothesis),
49
+ evidenceRefs: statementSet(evidenceRefs),
50
+ hypothesisIds: new Set(input.legacy ? [] : (input.hypotheses ?? []).map((hypothesis) => hypothesis.id)),
51
+ };
52
+ }
53
+
54
+ function hasNewValue(previousSet, currentSet) {
55
+ for (const value of currentSet) {
56
+ if (!previousSet.has(value)) return true;
57
+ }
58
+ return false;
59
+ }
60
+
61
+ export function compareDiagnosticCycles(previousInput, currentInput, context = {}) {
62
+ const previous = previousInput instanceof Set || Array.isArray(previousInput)
63
+ ? normalizeDiagnosticSnapshot(null)
64
+ : normalizeDiagnosticSnapshot(previousInput);
65
+ const current = normalizeDiagnosticSnapshot(currentInput);
66
+
67
+ const dimensions = {
68
+ newObservation: hasNewValue(previous.observationStatements, current.observationStatements),
69
+ newContributor: hasNewValue(previous.contributorStatements, current.contributorStatements),
70
+ newHypothesis: hasNewValue(previous.hypothesisStatements, current.hypothesisStatements),
71
+ hypothesisDispositionChanged: Boolean(context.hypothesisDispositionChanged),
72
+ failureSignatureChanged: Boolean(context.failureSignatureChanged),
73
+ failureSurfaceChanged: Boolean(context.failureSurfaceChanged),
74
+ interventionChanged: Boolean(context.interventionChanged),
75
+ strategyChanged: Boolean(context.strategyChanged),
76
+ newEvidence: hasNewValue(previous.evidenceRefs, current.evidenceRefs),
77
+ // Elimination is decided semantically by the cycle analysis projection:
78
+ // an id disappearing while its statement survives is ID-only churn, not gain.
79
+ hypothesisEliminated: Boolean(context.hypothesisEliminated),
80
+ };
81
+ return dimensions;
82
+ }
83
+
84
+ const CLASSIFICATION = Object.freeze({
85
+ FIRST_DIAGNOSIS: "FIRST_DIAGNOSIS",
86
+ NEW_HYPOTHESIS: "NEW_HYPOTHESIS",
87
+ NEW_EVIDENCE: "NEW_EVIDENCE",
88
+ NEW_HYPOTHESIS_AND_EVIDENCE: "NEW_HYPOTHESIS_AND_EVIDENCE",
89
+ NONE: "NONE",
90
+ });
91
+
92
+ export function classifyGain(dimensions, { first = false } = {}) {
93
+ if (first) return CLASSIFICATION.FIRST_DIAGNOSIS;
94
+ const hypothesis = dimensions.newHypothesis;
95
+ const evidence = dimensions.newEvidence;
96
+ if (hypothesis && evidence) return CLASSIFICATION.NEW_HYPOTHESIS_AND_EVIDENCE;
97
+ if (hypothesis) return CLASSIFICATION.NEW_HYPOTHESIS;
98
+ if (evidence) return CLASSIFICATION.NEW_EVIDENCE;
99
+ return CLASSIFICATION.NONE;
100
+ }
101
+
102
+ export function isEffectiveGain(dimensions, classification) {
103
+ if (classification !== CLASSIFICATION.NONE) return true;
104
+
105
+ // Every meaningful semantic dimension counts as effective gain.
106
+ // newHypothesis and newEvidence remain covered by the compatibility
107
+ // classification above; the remaining dimensions are checked explicitly.
108
+ return Boolean(
109
+ dimensions.newObservation
110
+ || dimensions.newContributor
111
+ || dimensions.hypothesisDispositionChanged
112
+ || dimensions.failureSignatureChanged
113
+ || dimensions.failureSurfaceChanged
114
+ || dimensions.interventionChanged
115
+ || dimensions.strategyChanged
116
+ || dimensions.hypothesisEliminated
117
+ );
118
+ }
119
+
120
+ export function computeInformationGain(entries) {
121
+ const results = [];
122
+ let previous = null;
123
+ let seenFirst = false;
124
+ for (const entry of entries) {
125
+ const dimensions = compareDiagnosticCycles(previous?.snapshot ?? null, entry.snapshot, entry.context ?? {});
126
+ const classification = classifyGain(dimensions, { first: !seenFirst });
127
+ seenFirst = true;
128
+ results.push({
129
+ verificationCycle: entry.verificationCycle,
130
+ sequence: entry.sequence ?? null,
131
+ effectiveGain: isEffectiveGain(dimensions, classification),
132
+ classification,
133
+ dimensions,
134
+ });
135
+ previous = entry;
136
+ }
137
+ return results;
138
+ }
@@ -11,6 +11,100 @@ import { FORGELOOP_KIT_DIR } from "./target-layout.js";
11
11
  import { trustedAuthorityConfiguration } from "./trusted-authority.js";
12
12
  import { reconcileContinuity } from "./continuity-reconciliation.js";
13
13
  import { continuityFinding, continuityIsHealthy } from "./continuity-observability.js";
14
+ import { findTaskById } from "./task-discovery.js";
15
+ import { buildTaskTrace } from "./trace.js";
16
+ import { evaluateProgress } from "./progress.js";
17
+ import { readEvents } from "./events.js";
18
+
19
+ function reasonCodesFor({ state, trace, progress }) {
20
+ const codes = [];
21
+ codes.push(trace.integrity.valid ? "LEDGER_VALID" : "LEDGER_INCONSISTENT");
22
+ if (state?.status === "VALID" || state?.status === "FRESH") codes.push("REPOSITORY_FRESH");
23
+ if (["STALE", "REVALIDATION_REQUIRED"].includes(state?.status)) codes.push("REPOSITORY_STALE");
24
+ if (trace.task.phase === "COMPLETE") codes.push("COMPLETION_VALID");
25
+ else codes.push("COMPLETION_INCOMPLETE");
26
+ if (progress.status === "ADVANCING") codes.push("DIAGNOSTIC_PROGRESS_ADVANCING");
27
+ if (progress.status === "STALLED") codes.push("DIAGNOSTIC_PROGRESS_STALLED");
28
+ return codes;
29
+ }
30
+
31
+ async function buildTaskInspection({ target, packageRoot, taskId, state, classifiedStatus = null }) {
32
+ const trace = await buildTaskTrace({ target, packageRoot, taskId });
33
+ const events = await readEvents(target, packageRoot, { taskId });
34
+ const progress = evaluateProgress({ state, events });
35
+ const failedRequirements = [...new Set(
36
+ trace.checks
37
+ .filter((check) => check.currentResult === "failed" || check.currentResult === "blocked")
38
+ .map((check) => check.requirement ?? check.id),
39
+ )].sort();
40
+
41
+ const issues = [];
42
+ if (!trace.snapshot.consistent) {
43
+ issues.push({ code: "E_TRACE_SNAPSHOT_INCONSISTENT", message: "Task artifacts changed while being read; rerun inspect for a consistent view." });
44
+ }
45
+ for (const error of trace.integrity.errors) {
46
+ issues.push({ code: error.code ?? "E_EVENT_INVALID", message: error.message });
47
+ }
48
+ if (failedRequirements.length > 0 && trace.diagnostics.cases.length === 0 && trace.diagnostics.legacyDiagnoses.length === 0) {
49
+ issues.push({ code: "E_DIAGNOSIS_REQUIRED", message: "Failed requirements have no recorded diagnosis." });
50
+ }
51
+
52
+ const explanation = {
53
+ result: failedRequirements.length > 0 ? "INCOMPLETE_VERIFICATION" : (trace.task.phase === "COMPLETE" ? "COMPLETE" : "INCOMPLETE"),
54
+ reasons: reasonCodesFor({ state: { status: classifiedStatus }, trace, progress }),
55
+ };
56
+
57
+ return {
58
+ task: taskId,
59
+ snapshot: {
60
+ consistent: trace.snapshot.consistent,
61
+ stateRevision: trace.snapshot.stateRevision,
62
+ ledgerTailSequence: trace.snapshot.ledgerTailSequence,
63
+ },
64
+ lifecycle: {
65
+ phase: trace.task.phase,
66
+ verificationCycle: trace.task.verificationCycle,
67
+ transitions: trace.transitions,
68
+ },
69
+ history: {
70
+ eventCount: trace.events.length,
71
+ quality: trace.historyQuality,
72
+ },
73
+ verification: {
74
+ checks: trace.checks.map((check) => ({
75
+ id: check.id,
76
+ requirement: check.requirement,
77
+ currentResult: check.currentResult,
78
+ attemptCount: check.attemptCount,
79
+ failedAttempts: check.failedAttempts,
80
+ })),
81
+ failedRequirements,
82
+ },
83
+ diagnostics: {
84
+ legacyDiagnosisCount: trace.diagnostics.legacyDiagnoses.length,
85
+ diagnosticCaseCount: trace.diagnostics.cases.length,
86
+ interventionCount: trace.diagnostics.interventions.length,
87
+ dispositionCount: trace.diagnostics.dispositions.length,
88
+ latestCase: trace.diagnostics.cases.at(-1) ?? null,
89
+ },
90
+ progress,
91
+ failureSurfaces: trace.failureSurfaces,
92
+ failureSignatures: trace.failureSignatures.map((entry) => ({
93
+ signature: entry.signature,
94
+ requirements: entry.requirements,
95
+ cycles: entry.cycles,
96
+ })),
97
+ integrity: {
98
+ valid: trace.integrity.valid,
99
+ errors: trace.integrity.errors,
100
+ },
101
+ audit: {},
102
+ completion: {},
103
+ issues,
104
+ explanation,
105
+ next: { command: `forgeloop next --task ${taskId} --json` },
106
+ };
107
+ }
14
108
 
15
109
  function profileMetadata(bytes) {
16
110
  const text = bytes.toString("utf8");
@@ -39,7 +133,9 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
39
133
  const effectiveStateRel = stateFile ?? (taskId ? taskArtifactPath(taskId, "state") : WORK_STATE_PATH);
40
134
  const statePath = ensureWithin(target, effectiveStateRel);
41
135
  const statePresent = await fileExists(statePath);
42
- const state = await readAndClassifyWorkState({ target, packageRoot, contractFile, taskId, stateFile: effectiveStateRel });
136
+ const classifiedState = await readAndClassifyWorkState({ target, packageRoot, contractFile, taskId, stateFile: effectiveStateRel });
137
+ const rawState = classifiedState?.state ?? null;
138
+ const taskInfo = taskId ? await findTaskById(target, taskId, packageRoot) : null;
43
139
  const continuity = await reconcileContinuity({ target, packageRoot, taskId });
44
140
  const schemaRoot = manifest?.layoutVersion >= 2
45
141
  ? ensureWithin(target, FORGELOOP_KIT_DIR)
@@ -78,12 +174,27 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
78
174
  const continuityIssue = continuityFinding(continuity);
79
175
  if (continuityIssue) findings.push(continuityIssue);
80
176
 
81
- if (state.status === "INVALID") {
177
+ if (taskInfo?.ownershipValid === false) {
178
+ findings.push({
179
+ code: "task-claim-ownership-inconsistent",
180
+ severity: "error",
181
+ path: taskArtifactPath(taskId, "recovery"),
182
+ message: "Task claim ownership cannot be validated from recovery state and ledger history.",
183
+ remediation: `Run forgeloop validate-protocol --task ${taskId} --json and repair the reported protocol-owned artifact.`,
184
+ evidence: createEvidence({
185
+ kind: "BLOCKED",
186
+ source: taskArtifactPath(taskId, "events"),
187
+ result: "E_TASK_CLAIM_OWNERSHIP_INCONSISTENT",
188
+ }),
189
+ });
190
+ }
191
+
192
+ if (classifiedState.status === "INVALID") {
82
193
  findings.push({
83
194
  code: "state-invalid",
84
195
  severity: "error",
85
- path: WORK_STATE_PATH,
86
- message: state.error ?? "Work state is invalid.",
196
+ path: effectiveStateRel,
197
+ message: classifiedState.error ?? "Work state is invalid.",
87
198
  remediation: "Repair or clear the checkpoint after reviewing the parse error.",
88
199
  evidence: createEvidence({ kind: "BLOCKED", source: WORK_STATE_PATH, result: "invalid" }),
89
200
  });
@@ -96,9 +207,12 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
96
207
  })];
97
208
  const evidence = [
98
209
  ...(doctor.evidence ?? []),
99
- ...(state.evidence ?? []),
210
+ ...(classifiedState.evidence ?? []),
100
211
  ...protocolEvidence,
101
212
  ];
213
+ const taskInspection = taskId
214
+ ? await buildTaskInspection({ target, packageRoot, taskId, state: rawState, classifiedStatus: classifiedState.status })
215
+ : null;
102
216
  return {
103
217
  target: { path: target },
104
218
  authority: trustedAuthorityConfiguration({ target, authorityContext, runtimeContext }),
@@ -131,7 +245,16 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
131
245
  schemas: schemaHealth.schemas,
132
246
  evidence: protocolEvidence,
133
247
  },
134
- state: { ...state, path: WORK_STATE_PATH, present: statePresent },
248
+ state: { ...classifiedState, path: effectiveStateRel, present: statePresent },
249
+ recovery: taskInfo?.recovery ?? null,
250
+ claims: taskInfo ? {
251
+ state: taskInfo.claimState,
252
+ historical: taskInfo.historicalWriteClaims,
253
+ effective: taskInfo.effectiveWriteClaims,
254
+ mutationAllowed: taskInfo.mutationAllowed,
255
+ ownershipValid: taskInfo.ownershipValid,
256
+ ownershipErrors: taskInfo.ownershipErrors ?? taskInfo.errors ?? [],
257
+ } : null,
135
258
  continuity,
136
259
  compatibility: {
137
260
  deprecated: true,
@@ -139,10 +262,12 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
139
262
  },
140
263
  findings,
141
264
  evidence,
265
+ ...(taskInspection ? { taskInspection } : {}),
142
266
  ok: doctor.ok
143
267
  && !manifestError
144
268
  && schemaHealth.status === "valid"
145
- && !["INVALID", "REVALIDATION_REQUIRED"].includes(state.status)
269
+ && taskInfo?.ownershipValid !== false
270
+ && !["INVALID", "REVALIDATION_REQUIRED"].includes(classifiedState.status)
146
271
  && continuityIsHealthy(continuity),
147
272
  };
148
273
  }
@@ -0,0 +1,217 @@
1
+ import { CLI_COMMAND_DEFINITIONS } from "./cli-command-definitions.js";
2
+ import { COMMAND_EXECUTORS } from "./command-executors.js";
3
+ import { PROTOCOL_VERSION } from "./protocol.js";
4
+ import { FORGELOOP_INTEGRATION_RUNTIME_VERSION } from "./command-runtime.js";
5
+
6
+ /**
7
+ * Integration risk classes. They classify the *invocation*, not only the
8
+ * command name: input-dependent commands (doctor --fix, task-unlock --force,
9
+ * policy-discover --write, baseline mutations) are refined by the sparse
10
+ * override table below.
11
+ */
12
+ export const INTEGRATION_RISK_CLASSES = Object.freeze({
13
+ READ_ONLY: "READ_ONLY",
14
+ LOOP_MUTATION: "LOOP_MUTATION",
15
+ CLAIM_REACQUISITION: "CLAIM_REACQUISITION",
16
+ EXTERNAL_EXECUTION: "EXTERNAL_EXECUTION",
17
+ AUTHORITY_MUTATION: "AUTHORITY_MUTATION",
18
+ EXTERNAL_STATE_ATTESTATION: "EXTERNAL_STATE_ATTESTATION",
19
+ MAINTENANCE: "MAINTENANCE",
20
+ CLAIM_RELEASE_RECOVERY: "CLAIM_RELEASE_RECOVERY",
21
+ LEGACY_MIGRATION: "LEGACY_MIGRATION",
22
+ FORCE_DESTRUCTIVE: "FORCE_DESTRUCTIVE",
23
+ });
24
+
25
+ const READ_ONLY_COMMANDS = Object.freeze(new Set([
26
+ "protocol-info", "status", "next", "continuity", "reconcile-continuity",
27
+ "task-list", "task-show", "task-lock-status", "progress", "audit", "report",
28
+ "inspect", "validate-state", "validate-protocol", "validate-receipt",
29
+ "policy-status", "policy-diff", "rule-verify", "policy",
30
+ "history", "trace", "reflect",
31
+ ]));
32
+
33
+ const LOOP_MUTATION_COMMANDS = Object.freeze(new Set([
34
+ "route", "preflight", "advance", "task-create", "task-scope",
35
+ "record-continuity", "clear-continuity", "prepare-completion",
36
+ "record-check", "record-diagnosis", "record-decision-criterion",
37
+ "record-terminal-result", "complete",
38
+ "record-intervention", "record-hypothesis-disposition",
39
+ ]));
40
+
41
+ const STATIC_RISK_CLASSES = Object.freeze({
42
+ ...Object.fromEntries([...READ_ONLY_COMMANDS].map((name) => [name, INTEGRATION_RISK_CLASSES.READ_ONLY])),
43
+ ...Object.fromEntries([...LOOP_MUTATION_COMMANDS].map((name) => [name, INTEGRATION_RISK_CLASSES.LOOP_MUTATION])),
44
+ "task-resume": INTEGRATION_RISK_CLASSES.CLAIM_REACQUISITION,
45
+ "run-check": INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
46
+ "run-action": INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
47
+ "reconcile-closure": INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
48
+ "action-propose": INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
49
+ "action-authorize": INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
50
+ "action-record": INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
51
+ "action-verify": INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
52
+ "action-reconcile": INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
53
+ "approval-request": INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
54
+ "approval-resolve": INTEGRATION_RISK_CLASSES.AUTHORITY_MUTATION,
55
+ eval: INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
56
+ "action-show": INTEGRATION_RISK_CLASSES.READ_ONLY,
57
+ metrics: INTEGRATION_RISK_CLASSES.READ_ONLY,
58
+ init: INTEGRATION_RISK_CLASSES.MAINTENANCE,
59
+ update: INTEGRATION_RISK_CLASSES.MAINTENANCE,
60
+ activate: INTEGRATION_RISK_CLASSES.MAINTENANCE,
61
+ "task-migrate": INTEGRATION_RISK_CLASSES.MAINTENANCE,
62
+ "migrate-protocol": INTEGRATION_RISK_CLASSES.MAINTENANCE,
63
+ "clear-state": INTEGRATION_RISK_CLASSES.MAINTENANCE,
64
+ doctor: INTEGRATION_RISK_CLASSES.MAINTENANCE,
65
+ "policy-discover": INTEGRATION_RISK_CLASSES.MAINTENANCE,
66
+ baseline: INTEGRATION_RISK_CLASSES.MAINTENANCE,
67
+ "task-unlock": INTEGRATION_RISK_CLASSES.MAINTENANCE,
68
+ // bundle writes a bundle artifact set under the task namespace; it is not
69
+ // read-only despite producing no protocol-state mutations.
70
+ bundle: INTEGRATION_RISK_CLASSES.MAINTENANCE,
71
+ "profile-interview": INTEGRATION_RISK_CLASSES.MAINTENANCE,
72
+ "task-recover": INTEGRATION_RISK_CLASSES.CLAIM_RELEASE_RECOVERY,
73
+ "task-repair-legacy-recovery": INTEGRATION_RISK_CLASSES.LEGACY_MIGRATION,
74
+ });
75
+
76
+ // Sparse input-dependent refinements over the static table.
77
+ function refineRiskClass(command, input) {
78
+ if (command === "task-unlock" && input?.force === true) {
79
+ return INTEGRATION_RISK_CLASSES.FORCE_DESTRUCTIVE;
80
+ }
81
+ // Settling external commit state is independently gated: recording an
82
+ // UNKNOWN observation stays LOOP_MUTATION, while COMMITTED / NOT_COMMITTED
83
+ // settlements attest external state and require their own capability.
84
+ if (
85
+ command === "action-reconcile"
86
+ && ["COMMITTED", "NOT_COMMITTED"].includes(input?.reconciliationOutcome)
87
+ ) {
88
+ return INTEGRATION_RISK_CLASSES.EXTERNAL_STATE_ATTESTATION;
89
+ }
90
+ // Fail closed: every canonical command must be explicitly classified.
91
+ return baseRiskClass(command);
92
+ }
93
+
94
+ export function baseRiskClass(command) {
95
+ const riskClass = STATIC_RISK_CLASSES[command];
96
+ if (!riskClass) {
97
+ throw new Error(`Command ${command} has no integration risk classification`);
98
+ }
99
+ return riskClass;
100
+ }
101
+
102
+ export function getForgeLoopCapabilities({ packageVersion = null } = {}) {
103
+ const commands = Object.keys(CLI_COMMAND_DEFINITIONS).sort().map((name) => {
104
+ const def = CLI_COMMAND_DEFINITIONS[name];
105
+ return {
106
+ name,
107
+ category: def.category,
108
+ mutation: def.mutation,
109
+ baseRiskClass: baseRiskClass(name),
110
+ mayExecuteExternalProcess: def.mayExecuteExternalProcess === true,
111
+ description: def.description,
112
+ };
113
+ });
114
+ return {
115
+ packageVersion,
116
+ protocolVersion: PROTOCOL_VERSION,
117
+ integrationApiVersion: FORGELOOP_INTEGRATION_RUNTIME_VERSION,
118
+ executorParity: Object.keys(COMMAND_EXECUTORS).length === Object.keys(CLI_COMMAND_DEFINITIONS).length,
119
+ features: {
120
+ taskClaimRecovery: {
121
+ version: 1,
122
+ durableRecoveryState: true,
123
+ explicitResume: true,
124
+ validatedClaimProjection: true,
125
+ },
126
+ durableActions: {
127
+ version: 1,
128
+ readOnlyResources: true,
129
+ externalExecutionOverMcp: false,
130
+ },
131
+ trajectoryEvaluation: {
132
+ version: 1,
133
+ readOnlyMetrics: true,
134
+ projectLocalReference: true,
135
+ },
136
+ },
137
+ commands,
138
+ resources: [
139
+ { name: "protocol/info", scope: "PROJECT" },
140
+ { name: "project/tasks", scope: "PROJECT" },
141
+ { name: "task/status", scope: "TASK" },
142
+ { name: "task/ownership", scope: "TASK" },
143
+ { name: "task/contract", scope: "TASK" },
144
+ { name: "task/continuity", scope: "TASK" },
145
+ { name: "task/actions", scope: "TASK" },
146
+ { name: "task/action", scope: "TASK" },
147
+ { name: "task/approvals", scope: "TASK" },
148
+ { name: "task/metrics", scope: "TASK" },
149
+ { name: "task/evaluations", scope: "TASK" },
150
+ { name: "project/capability-policy", scope: "PROJECT" },
151
+ ],
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Classify a concrete command invocation (command + structured input).
157
+ * Tool-provided input can never elevate a launch-level capability; this
158
+ * classifier only describes what the invocation would do.
159
+ */
160
+ export function classifyForgeLoopInvocation(command, input = {}) {
161
+ const definition = CLI_COMMAND_DEFINITIONS[command];
162
+ if (!definition) {
163
+ throw new Error(`Unknown ForgeLoop command: ${command}`);
164
+ }
165
+ const riskClass = refineRiskClass(command, input);
166
+ const readOnly = riskClass === INTEGRATION_RISK_CLASSES.READ_ONLY;
167
+ const requiredCapability = (() => {
168
+ switch (riskClass) {
169
+ case INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION:
170
+ return "allowExternalExecution";
171
+ case INTEGRATION_RISK_CLASSES.AUTHORITY_MUTATION:
172
+ return "allowApprovalResolution";
173
+ case INTEGRATION_RISK_CLASSES.EXTERNAL_STATE_ATTESTATION:
174
+ return "allowActionReconciliationSettlement";
175
+ case INTEGRATION_RISK_CLASSES.MAINTENANCE:
176
+ return "allowMaintenance";
177
+ case INTEGRATION_RISK_CLASSES.CLAIM_RELEASE_RECOVERY:
178
+ return "allowRecovery";
179
+ case INTEGRATION_RISK_CLASSES.LEGACY_MIGRATION:
180
+ return "allowLegacyRepair";
181
+ case INTEGRATION_RISK_CLASSES.FORCE_DESTRUCTIVE:
182
+ return "allowForceRecovery";
183
+ default:
184
+ return null;
185
+ }
186
+ })();
187
+ return Object.freeze({
188
+ command,
189
+ riskClass,
190
+ readOnly,
191
+ mutatesProtocol: !readOnly && [
192
+ INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
193
+ INTEGRATION_RISK_CLASSES.CLAIM_REACQUISITION,
194
+ INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
195
+ INTEGRATION_RISK_CLASSES.AUTHORITY_MUTATION,
196
+ INTEGRATION_RISK_CLASSES.EXTERNAL_STATE_ATTESTATION,
197
+ INTEGRATION_RISK_CLASSES.MAINTENANCE,
198
+ INTEGRATION_RISK_CLASSES.CLAIM_RELEASE_RECOVERY,
199
+ INTEGRATION_RISK_CLASSES.LEGACY_MIGRATION,
200
+ INTEGRATION_RISK_CLASSES.FORCE_DESTRUCTIVE,
201
+ ].includes(riskClass),
202
+ removesArtifacts: definition.removes.length > 0,
203
+ executesExternalProcess: definition.mayExecuteExternalProcess === true,
204
+ affectsClaimAuthority: [
205
+ "task-resume", "task-recover", "task-repair-legacy-recovery",
206
+ "task-create", "task-scope", "complete",
207
+ ].includes(command),
208
+ destructive: [
209
+ INTEGRATION_RISK_CLASSES.AUTHORITY_MUTATION,
210
+ INTEGRATION_RISK_CLASSES.FORCE_DESTRUCTIVE,
211
+ INTEGRATION_RISK_CLASSES.CLAIM_RELEASE_RECOVERY,
212
+ INTEGRATION_RISK_CLASSES.LEGACY_MIGRATION,
213
+ INTEGRATION_RISK_CLASSES.MAINTENANCE,
214
+ ].includes(riskClass),
215
+ requiredCapability,
216
+ });
217
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Bounded input limits for structured integrations. These bound the
3
+ * transport-facing surface; canonical ForgeLoop JSON safety limits remain
4
+ * authoritative for persisted artifacts. Values are conservative and
5
+ * intentionally small for an agent-facing API.
6
+ */
7
+ export const INTEGRATION_LIMITS = Object.freeze({
8
+ /** Maximum length of any single string input (task IDs, names, text). */
9
+ maxStringLength: 4096,
10
+ /** Maximum number of entries in a repeatable string option. */
11
+ maxRepeatedValues: 32,
12
+ /** Maximum number of exact argv items passed to external execution. */
13
+ maxArgvItems: 64,
14
+ /** Maximum length of a single argv item. */
15
+ maxArgvItemLength: 2048,
16
+ /** Maximum serialized size of a JSON-object input field. */
17
+ maxStructuredInputBytes: 256 * 1024,
18
+ /** Maximum serialized size of any tool/resource response payload. */
19
+ maxOutputBytes: 4 * 1024 * 1024,
20
+ });