loopforge 3.9.4 → 4.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +236 -112
- package/dist/audit.d.ts +73 -15
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +131 -61
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +114 -40
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +276 -182
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +41 -12
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-governance.d.ts +0 -2
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +0 -4
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +97 -156
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +223 -419
- package/dist/committed-round.js.map +1 -1
- package/dist/constraint-lifecycle.d.ts +73 -0
- package/dist/constraint-lifecycle.d.ts.map +1 -0
- package/dist/constraint-lifecycle.js +174 -0
- package/dist/constraint-lifecycle.js.map +1 -0
- package/dist/context-boundary.d.ts +83 -0
- package/dist/context-boundary.d.ts.map +1 -0
- package/dist/context-boundary.js +101 -0
- package/dist/context-boundary.js.map +1 -0
- package/dist/contract-items.d.ts +12 -8
- package/dist/contract-items.d.ts.map +1 -1
- package/dist/contract-items.js +31 -26
- package/dist/contract-items.js.map +1 -1
- package/dist/contract-requirement.d.ts +47 -0
- package/dist/contract-requirement.d.ts.map +1 -0
- package/dist/contract-requirement.js +51 -0
- package/dist/contract-requirement.js.map +1 -0
- package/dist/criterion-status.d.ts +35 -0
- package/dist/criterion-status.d.ts.map +1 -0
- package/dist/criterion-status.js +89 -0
- package/dist/criterion-status.js.map +1 -0
- package/dist/enforcement-gate.d.ts +26 -38
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +196 -319
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +9 -103
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +27 -897
- package/dist/engine.js.map +1 -1
- package/dist/errors.d.ts +41 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +69 -0
- package/dist/errors.js.map +1 -0
- package/dist/evidence-claims.d.ts +33 -31
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +58 -72
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-posture-view.d.ts +19 -0
- package/dist/evidence-posture-view.d.ts.map +1 -0
- package/dist/evidence-posture-view.js +116 -0
- package/dist/evidence-posture-view.js.map +1 -0
- package/dist/evidence-posture.d.ts +73 -0
- package/dist/evidence-posture.d.ts.map +1 -0
- package/dist/evidence-posture.js +281 -0
- package/dist/evidence-posture.js.map +1 -0
- package/dist/evidence-provider.d.ts +55 -37
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +182 -332
- package/dist/evidence-provider.js.map +1 -1
- package/dist/explain.d.ts +166 -27
- package/dist/explain.d.ts.map +1 -1
- package/dist/explain.js +372 -58
- package/dist/explain.js.map +1 -1
- package/dist/field-accounting.d.ts.map +1 -1
- package/dist/field-accounting.js +3 -22
- package/dist/field-accounting.js.map +1 -1
- package/dist/generate-schema.js +13 -8
- package/dist/generate-schema.js.map +1 -1
- package/dist/git-boundary.d.ts +128 -0
- package/dist/git-boundary.d.ts.map +1 -0
- package/dist/git-boundary.js +625 -0
- package/dist/git-boundary.js.map +1 -0
- package/dist/git-facts.d.ts +22 -0
- package/dist/git-facts.d.ts.map +1 -0
- package/dist/git-facts.js +29 -0
- package/dist/git-facts.js.map +1 -0
- package/dist/git-motion.d.ts +37 -0
- package/dist/git-motion.d.ts.map +1 -0
- package/dist/git-motion.js +59 -0
- package/dist/git-motion.js.map +1 -0
- package/dist/history.d.ts +44 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +77 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +19 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +16 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +80 -14
- package/dist/init.js.map +1 -1
- package/dist/loop-compiler.d.ts +39 -23
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +368 -565
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-store.d.ts +6 -5
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +153 -35
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +255 -81
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +1552 -539
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +42 -18
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +81 -39
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +956 -282
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/submission-boundary.d.ts +23 -31
- package/dist/mcp/submission-boundary.d.ts.map +1 -1
- package/dist/mcp/submission-boundary.js +242 -92
- package/dist/mcp/submission-boundary.js.map +1 -1
- package/dist/mcp/submission-handle.d.ts +66 -0
- package/dist/mcp/submission-handle.d.ts.map +1 -0
- package/dist/mcp/submission-handle.js +173 -0
- package/dist/mcp/submission-handle.js.map +1 -0
- package/dist/mcp/tools.d.ts +17 -7
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +707 -244
- package/dist/mcp/tools.js.map +1 -1
- package/dist/open-question.d.ts +7 -0
- package/dist/open-question.d.ts.map +1 -0
- package/dist/open-question.js +14 -0
- package/dist/open-question.js.map +1 -0
- package/dist/policy-metrics.d.ts +31 -21
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +117 -64
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +11 -39
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +12 -12
- package/dist/policy.js.map +1 -1
- package/dist/progress.d.ts +45 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +136 -0
- package/dist/progress.js.map +1 -0
- package/dist/prompt-assembler.d.ts +20 -6
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +475 -479
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-coverage.d.ts +0 -6
- package/dist/prompt-coverage.d.ts.map +1 -1
- package/dist/prompt-coverage.js +4 -8
- package/dist/prompt-coverage.js.map +1 -1
- package/dist/prompt-freshness.d.ts +6 -17
- package/dist/prompt-freshness.d.ts.map +1 -1
- package/dist/prompt-freshness.js +27 -22
- package/dist/prompt-freshness.js.map +1 -1
- package/dist/prompt-policy.d.ts +14 -7
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +21 -5
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +843 -424
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +73 -102
- package/dist/protocol.js.map +1 -1
- package/dist/recovery-guidance.d.ts +13 -0
- package/dist/recovery-guidance.d.ts.map +1 -0
- package/dist/recovery-guidance.js +15 -0
- package/dist/recovery-guidance.js.map +1 -0
- package/dist/recovery.d.ts +68 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +234 -0
- package/dist/recovery.js.map +1 -0
- package/dist/replay.d.ts +42 -8
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +311 -84
- package/dist/replay.js.map +1 -1
- package/dist/result-facts.d.ts +45 -0
- package/dist/result-facts.d.ts.map +1 -0
- package/dist/result-facts.js +86 -0
- package/dist/result-facts.js.map +1 -0
- package/dist/round-contract.d.ts +38 -8
- package/dist/round-contract.d.ts.map +1 -1
- package/dist/round-contract.js +71 -31
- package/dist/round-contract.js.map +1 -1
- package/dist/round-coordinator.d.ts +41 -89
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +250 -267
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +56 -16
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +225 -39
- package/dist/round-driver.js.map +1 -1
- package/dist/round-facts.d.ts +33 -52
- package/dist/round-facts.d.ts.map +1 -1
- package/dist/round-facts.js +51 -160
- package/dist/round-facts.js.map +1 -1
- package/dist/round-guidance.d.ts +56 -0
- package/dist/round-guidance.d.ts.map +1 -0
- package/dist/round-guidance.js +161 -0
- package/dist/round-guidance.js.map +1 -0
- package/dist/round-outcome.d.ts +21 -0
- package/dist/round-outcome.d.ts.map +1 -0
- package/dist/round-outcome.js +31 -0
- package/dist/round-outcome.js.map +1 -0
- package/dist/round-transaction.d.ts +168 -21
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +837 -38
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +37 -61
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +413 -392
- package/dist/self-eval.js.map +1 -1
- package/dist/storage.d.ts +16 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +54 -4
- package/dist/storage.js.map +1 -1
- package/dist/subgoal-state.d.ts +2 -3
- package/dist/subgoal-state.d.ts.map +1 -1
- package/dist/subgoal-state.js +2 -3
- package/dist/subgoal-state.js.map +1 -1
- package/dist/submission-guidance.d.ts +29 -21
- package/dist/submission-guidance.d.ts.map +1 -1
- package/dist/submission-guidance.js +308 -260
- package/dist/submission-guidance.js.map +1 -1
- package/dist/submission-receipt.d.ts +16 -8
- package/dist/submission-receipt.d.ts.map +1 -1
- package/dist/submission-receipt.js +145 -51
- package/dist/submission-receipt.js.map +1 -1
- package/dist/token-utils.d.ts +28 -7
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +27 -11
- package/dist/token-utils.js.map +1 -1
- package/dist/transaction-identity.d.ts +9 -0
- package/dist/transaction-identity.d.ts.map +1 -0
- package/dist/transaction-identity.js +69 -0
- package/dist/transaction-identity.js.map +1 -0
- package/dist/verification-actions.d.ts.map +1 -1
- package/dist/verification-actions.js +10 -11
- package/dist/verification-actions.js.map +1 -1
- package/dist/verification-gate.d.ts +23 -47
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +202 -423
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.d.ts +187 -0
- package/dist/working-memory.d.ts.map +1 -0
- package/dist/working-memory.js +767 -0
- package/dist/working-memory.js.map +1 -0
- package/dist/workspace-checkpoint.d.ts +88 -0
- package/dist/workspace-checkpoint.d.ts.map +1 -0
- package/dist/workspace-checkpoint.js +326 -0
- package/dist/workspace-checkpoint.js.map +1 -0
- package/dist/workspace-restore.d.ts +68 -0
- package/dist/workspace-restore.d.ts.map +1 -0
- package/dist/workspace-restore.js +604 -0
- package/dist/workspace-restore.js.map +1 -0
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +0 -17
- package/dist/workspace.js.map +1 -1
- package/loop_policy.json +4 -18
- package/package.json +68 -68
- package/skills/forge/SKILL.md +212 -0
- package/skills/forge/references/evidence-and-contracts.md +204 -0
- package/skills/forge/references/recovery-and-views.md +188 -0
- package/dist/cognitive-facts.d.ts +0 -30
- package/dist/cognitive-facts.d.ts.map +0 -1
- package/dist/cognitive-facts.js +0 -113
- package/dist/cognitive-facts.js.map +0 -1
- package/dist/loop-extras-parser.d.ts +0 -81
- package/dist/loop-extras-parser.d.ts.map +0 -1
- package/dist/loop-extras-parser.js +0 -213
- package/dist/loop-extras-parser.js.map +0 -1
- package/dist/loop-projection.d.ts +0 -6
- package/dist/loop-projection.d.ts.map +0 -1
- package/dist/loop-projection.js +0 -28
- package/dist/loop-projection.js.map +0 -1
- package/skills/perception/SKILL.md +0 -389
|
@@ -10,11 +10,18 @@
|
|
|
10
10
|
* execution (v2.0.1), plus explicitly configured shell-free command providers.
|
|
11
11
|
*/
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
|
-
import {
|
|
14
|
-
import { spawn
|
|
13
|
+
import { readFileSync, realpathSync, statSync, } from "node:fs";
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
15
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
16
16
|
import { GIT_OBSERVATION_SCHEMA_VERSION } from "./protocol.js";
|
|
17
|
+
import { GIT_WORKSPACE_COVERAGE, gitMotionSignal, observedGitHead, } from "./git-motion.js";
|
|
17
18
|
import { containInWorkspace } from "./workspace.js";
|
|
19
|
+
import { isRecord } from "./token-utils.js";
|
|
20
|
+
import { captureBoundary, GitCaptureFailure } from "./git-boundary.js";
|
|
21
|
+
// v3.9.8: the Git boundary implementation moved to git-boundary.ts so evidence
|
|
22
|
+
// and workspace checkpoints share ONE scan. These re-exports keep the existing
|
|
23
|
+
// test/library surface stable.
|
|
24
|
+
export { captureGitFileStateAsync } from "./git-boundary.js";
|
|
18
25
|
import { commandConfigHash, deriveConfiguredCapability, getPolicy, resolveStateDirectory } from "./policy.js";
|
|
19
26
|
import { logEvent } from "./observability.js";
|
|
20
27
|
import { policyMetrics } from "./policy-metrics.js";
|
|
@@ -118,6 +125,18 @@ export function capabilityWarnings(configured, policy, observed) {
|
|
|
118
125
|
"cannot be machine-verified; contract closure will require an explicit " +
|
|
119
126
|
"blocked outcome.");
|
|
120
127
|
}
|
|
128
|
+
// v3.9.9: the stall rule selects Git workspace coverage explicitly. Without
|
|
129
|
+
// the Git provider configured there is no workspace evidence basis for a
|
|
130
|
+
// stall verdict — other providers' facts remain visible but cannot affect
|
|
131
|
+
// that rule. This is a fact statement, not a blocker.
|
|
132
|
+
const providerNames = Array.isArray(policy.evidence.providers)
|
|
133
|
+
? policy.evidence.providers
|
|
134
|
+
: [];
|
|
135
|
+
if (!providerNames.includes("git")) {
|
|
136
|
+
warnings.push("The Git workspace evidence provider is not configured — the stall rule " +
|
|
137
|
+
"has no workspace evidence basis this loop. Progress facts from other " +
|
|
138
|
+
"providers are recorded and displayed but cannot affect stall decisions.");
|
|
139
|
+
}
|
|
121
140
|
// v3.8.2: a configured command whose harness is named in package.json. The
|
|
122
141
|
// entrypoint set for `npm test` is ["package.json"], so the test files the
|
|
123
142
|
// script actually runs are never in it: editing one is a warning, not a
|
|
@@ -191,12 +210,18 @@ function unregisteredProvider(name) {
|
|
|
191
210
|
}
|
|
192
211
|
/** v3.8: Build the observation a failed capture resolves to. The status names
|
|
193
212
|
* the machine fact; `detail` explains it for the audit trail. */
|
|
213
|
+
/** v3.9.9: THE per-kind base observation schema table. Custom observations
|
|
214
|
+
* keep base 1; only their nested progress snapshot moved to inner 2. */
|
|
215
|
+
export function expectedObservationSchemaVersion(kind) {
|
|
216
|
+
return kind === "git" ? GIT_OBSERVATION_SCHEMA_VERSION : 1;
|
|
217
|
+
}
|
|
194
218
|
function syntheticObservation(providerId, kind, phase, startedAt, status, detail) {
|
|
195
219
|
return {
|
|
196
220
|
// v3.9.1: a failed Git capture still carries the current Git observation
|
|
197
221
|
// schema so the committed snapshot can be parsed; its non-observed status
|
|
198
|
-
// is what tells consumers the boundary is unavailable.
|
|
199
|
-
|
|
222
|
+
// is what tells consumers the boundary is unavailable. Every synthetic
|
|
223
|
+
// observation goes through the ONE per-kind table.
|
|
224
|
+
schemaVersion: expectedObservationSchemaVersion(kind),
|
|
200
225
|
providerId,
|
|
201
226
|
kind,
|
|
202
227
|
phase,
|
|
@@ -207,6 +232,31 @@ function syntheticObservation(providerId, kind, phase, startedAt, status, detail
|
|
|
207
232
|
data: { detail },
|
|
208
233
|
};
|
|
209
234
|
}
|
|
235
|
+
export function observationVersionIssue(kind, schemaVersion, data, _status) {
|
|
236
|
+
if (kind !== "git" && kind !== "command" && kind !== "custom") {
|
|
237
|
+
return {
|
|
238
|
+
envelope: "observation",
|
|
239
|
+
schemaVersion: typeof schemaVersion === "number" ? schemaVersion : -1,
|
|
240
|
+
reason: "schema",
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (!isRecord(data)) {
|
|
244
|
+
return {
|
|
245
|
+
envelope: "observation",
|
|
246
|
+
schemaVersion: typeof schemaVersion === "number" ? schemaVersion : -1,
|
|
247
|
+
reason: "malformed",
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const expected = expectedObservationSchemaVersion(kind);
|
|
251
|
+
if (schemaVersion !== expected) {
|
|
252
|
+
return {
|
|
253
|
+
envelope: "observation",
|
|
254
|
+
schemaVersion: typeof schemaVersion === "number" ? schemaVersion : -1,
|
|
255
|
+
reason: "schema",
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
210
260
|
/** Collects observations from all configured providers (always async).
|
|
211
261
|
*
|
|
212
262
|
* v3.8: every configured provider produces exactly one observation per
|
|
@@ -255,8 +305,18 @@ export class EvidenceCollector {
|
|
|
255
305
|
const capture = Promise.resolve(provider.capture({
|
|
256
306
|
signal: controller.signal,
|
|
257
307
|
timeoutMs,
|
|
258
|
-
loopId: options.loopId,
|
|
308
|
+
...(options.loopId !== undefined ? { loopId: options.loopId } : {}),
|
|
259
309
|
phase,
|
|
310
|
+
...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
|
|
311
|
+
...(options.materializer
|
|
312
|
+
? { materializer: options.materializer }
|
|
313
|
+
: {}),
|
|
314
|
+
...(options.boundarySink
|
|
315
|
+
? { boundarySink: options.boundarySink }
|
|
316
|
+
: {}),
|
|
317
|
+
...(options.maxPaths !== undefined
|
|
318
|
+
? { maxPaths: options.maxPaths }
|
|
319
|
+
: {}),
|
|
260
320
|
}));
|
|
261
321
|
const observation = timeoutMs > 0
|
|
262
322
|
? await Promise.race([
|
|
@@ -277,10 +337,12 @@ export class EvidenceCollector {
|
|
|
277
337
|
}
|
|
278
338
|
if (!observation) {
|
|
279
339
|
policyMetrics.recordEvidence(provider.name, "unavailable", Date.now() - startedAt, options.loopId);
|
|
280
|
-
|
|
340
|
+
const unavailable = syntheticObservation(provider.name, provider.kind, phase, startedAt, "unavailable", "provider reported no observation");
|
|
341
|
+
return unavailable;
|
|
281
342
|
}
|
|
282
|
-
|
|
283
|
-
|
|
343
|
+
const normalized = observation;
|
|
344
|
+
policyMetrics.recordEvidence(provider.name, normalized.status === "timeout" ? "timeout" : "available", Date.now() - startedAt, options.loopId, normalized.status);
|
|
345
|
+
return normalized;
|
|
284
346
|
}
|
|
285
347
|
catch (error) {
|
|
286
348
|
policyMetrics.recordEvidence(provider.name, "failure", Date.now() - startedAt, options.loopId);
|
|
@@ -580,7 +642,7 @@ export class CommandEvidenceProvider {
|
|
|
580
642
|
observation(phase, status, exitCode, signal, stdout, stderr, truncated, durationMs, entrypointFiles, failureDetail, stdoutSha256, stderrSha256) {
|
|
581
643
|
const finishedAt = Date.now();
|
|
582
644
|
return {
|
|
583
|
-
schemaVersion:
|
|
645
|
+
schemaVersion: expectedObservationSchemaVersion("command"),
|
|
584
646
|
providerId: this.name,
|
|
585
647
|
kind: "command",
|
|
586
648
|
phase,
|
|
@@ -608,180 +670,8 @@ export class CommandEvidenceProvider {
|
|
|
608
670
|
};
|
|
609
671
|
}
|
|
610
672
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
constructor(status, message) {
|
|
614
|
-
super(message);
|
|
615
|
-
this.status = status;
|
|
616
|
-
this.name = "GitCaptureFailure";
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
const GIT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
620
|
-
/** Bounded file-hash concurrency. A module constant, not a policy knob. */
|
|
621
|
-
const BOUNDARY_HASH_CONCURRENCY = 16;
|
|
622
|
-
function splitNul(text) {
|
|
623
|
-
return text.split("\0").filter((entry) => entry.length > 0);
|
|
624
|
-
}
|
|
625
|
-
function uniqueSorted(values) {
|
|
626
|
-
return [...new Set(values)].sort();
|
|
627
|
-
}
|
|
628
|
-
/** Parse `git ls-files --stage -z`. A conflicted path can have several
|
|
629
|
-
* stages; stage 0 wins when present, otherwise the first entry is kept. The
|
|
630
|
-
* worktree fingerprint is what matters, so this is only used to recognise
|
|
631
|
-
* gitlinks and to carry the index object id. */
|
|
632
|
-
function parseStageEntries(text) {
|
|
633
|
-
const byPath = new Map();
|
|
634
|
-
for (const record of splitNul(text)) {
|
|
635
|
-
const tab = record.indexOf("\t");
|
|
636
|
-
if (tab < 0)
|
|
637
|
-
continue;
|
|
638
|
-
const [mode, object, stage] = record.slice(0, tab).split(" ");
|
|
639
|
-
if (!mode || !object || !stage)
|
|
640
|
-
continue;
|
|
641
|
-
const path = record.slice(tab + 1);
|
|
642
|
-
if (!path)
|
|
643
|
-
continue;
|
|
644
|
-
const existing = byPath.get(path);
|
|
645
|
-
if (!existing || stage === "0") {
|
|
646
|
-
byPath.set(path, { entry: { path, mode, object }, stage });
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
return [...byPath.values()]
|
|
650
|
-
.map((item) => item.entry)
|
|
651
|
-
.sort((a, b) => a.path.localeCompare(b.path));
|
|
652
|
-
}
|
|
653
|
-
/** v3.9.1: capture the complete boundary. All required git commands run in
|
|
654
|
-
* one parallel batch; HEAD is best-effort (a repository with no commit is a
|
|
655
|
-
* valid boundary with no head). Any required-command failure returns an
|
|
656
|
-
* explicit status — never an empty-but-observed boundary. */
|
|
657
|
-
export async function captureGitFileStateAsync(signal, timeoutMs,
|
|
658
|
-
/** Workspace root; injectable so tests can capture against a temp repo.
|
|
659
|
-
* Defaults to the process cwd, matching the evidence providers. */
|
|
660
|
-
cwd = process.cwd()) {
|
|
661
|
-
const timeout = timeoutMs ?? 15000;
|
|
662
|
-
const runGit = (args) => {
|
|
663
|
-
return new Promise((resolveGit, reject) => {
|
|
664
|
-
let settled = false;
|
|
665
|
-
const succeed = (stdout) => {
|
|
666
|
-
if (settled)
|
|
667
|
-
return;
|
|
668
|
-
settled = true;
|
|
669
|
-
resolveGit(stdout);
|
|
670
|
-
};
|
|
671
|
-
const fail = (error) => {
|
|
672
|
-
if (settled)
|
|
673
|
-
return;
|
|
674
|
-
settled = true;
|
|
675
|
-
reject(error);
|
|
676
|
-
};
|
|
677
|
-
// M1: core.quotePath=false — git otherwise octal-escapes every path
|
|
678
|
-
// with bytes >= 0x80. With -z, paths are NUL-delimited raw bytes; the
|
|
679
|
-
// flag is retained for the HEAD/plain outputs.
|
|
680
|
-
const child = execFile("git", ["-c", "core.quotePath=false", ...args], {
|
|
681
|
-
cwd,
|
|
682
|
-
encoding: "utf-8",
|
|
683
|
-
timeout,
|
|
684
|
-
signal,
|
|
685
|
-
maxBuffer: GIT_MAX_BUFFER,
|
|
686
|
-
windowsHide: true,
|
|
687
|
-
});
|
|
688
|
-
let stdout = "";
|
|
689
|
-
let stderr = "";
|
|
690
|
-
child.stdout?.on("data", (chunk) => {
|
|
691
|
-
stdout += chunk;
|
|
692
|
-
});
|
|
693
|
-
child.stderr?.on("data", (chunk) => {
|
|
694
|
-
stderr += chunk;
|
|
695
|
-
});
|
|
696
|
-
child.on("close", (code) => {
|
|
697
|
-
if (code === 0)
|
|
698
|
-
succeed(stdout);
|
|
699
|
-
else if (code === null) {
|
|
700
|
-
// A killed process can close without an exit code. If the error
|
|
701
|
-
// event has not settled the promise yet, the measured cause is the
|
|
702
|
-
// deadline.
|
|
703
|
-
fail(new GitCaptureFailure("timeout", `git ${args[0]} was killed`));
|
|
704
|
-
}
|
|
705
|
-
else if (/not a git repository|not a repository/i.test(stderr)) {
|
|
706
|
-
fail(new GitCaptureFailure("unavailable", stderr.trim() || "not a git repository"));
|
|
707
|
-
}
|
|
708
|
-
else {
|
|
709
|
-
fail(new GitCaptureFailure("error", `git ${args[0]} exited ${code}${stderr ? `: ${stderr.trim().slice(0, 300)}` : ""}`));
|
|
710
|
-
}
|
|
711
|
-
});
|
|
712
|
-
child.on("error", (error) => {
|
|
713
|
-
if (signal?.aborted) {
|
|
714
|
-
fail(new GitCaptureFailure("aborted", `git ${args[0]} aborted`));
|
|
715
|
-
}
|
|
716
|
-
else if (error.code === "ENOENT") {
|
|
717
|
-
fail(new GitCaptureFailure("unavailable", "git executable was not found"));
|
|
718
|
-
}
|
|
719
|
-
else if (error.killed || error.signal) {
|
|
720
|
-
fail(new GitCaptureFailure("timeout", `git ${args[0]} timed out after ${timeout}ms`));
|
|
721
|
-
}
|
|
722
|
-
else if (/not a git repository/i.test(error.message)) {
|
|
723
|
-
fail(new GitCaptureFailure("unavailable", error.message));
|
|
724
|
-
}
|
|
725
|
-
else {
|
|
726
|
-
fail(new GitCaptureFailure("error", error.message));
|
|
727
|
-
}
|
|
728
|
-
});
|
|
729
|
-
});
|
|
730
|
-
};
|
|
731
|
-
try {
|
|
732
|
-
// allSettled, not all: on failure the other children must still finish
|
|
733
|
-
// before this capture returns, or Windows keeps the workspace directory
|
|
734
|
-
// locked (and every caller would leak child-process handles).
|
|
735
|
-
const settled = await Promise.allSettled([
|
|
736
|
-
runGit(["ls-files", "--stage", "-z"]),
|
|
737
|
-
runGit(["ls-files", "--others", "--exclude-standard", "-z"]),
|
|
738
|
-
runGit(["diff", "--name-only", "-z"]),
|
|
739
|
-
runGit(["diff", "--cached", "--name-only", "-z"]),
|
|
740
|
-
]);
|
|
741
|
-
const failures = settled
|
|
742
|
-
.filter((item) => item.status === "rejected")
|
|
743
|
-
.map((item) => item.reason)
|
|
744
|
-
.filter((error) => error instanceof GitCaptureFailure);
|
|
745
|
-
if (failures.length > 0) {
|
|
746
|
-
const failure = failures.find((item) => item.status === "aborted" || item.status === "timeout") ?? failures[0];
|
|
747
|
-
throw failure;
|
|
748
|
-
}
|
|
749
|
-
const rejected = settled.find((item) => item.status === "rejected");
|
|
750
|
-
if (rejected) {
|
|
751
|
-
throw new GitCaptureFailure("error", String(rejected.reason));
|
|
752
|
-
}
|
|
753
|
-
const [stageOut, untrackedOut, unstagedOut, stagedOut] = settled.map((item) => item.value);
|
|
754
|
-
let head;
|
|
755
|
-
try {
|
|
756
|
-
head = (await runGit(["rev-parse", "--verify", "HEAD"])).trim() || undefined;
|
|
757
|
-
}
|
|
758
|
-
catch (error) {
|
|
759
|
-
// A repository with no commit has no HEAD. Abort still propagates.
|
|
760
|
-
if (error instanceof GitCaptureFailure && error.status === "aborted")
|
|
761
|
-
throw error;
|
|
762
|
-
}
|
|
763
|
-
const untracked = uniqueSorted(splitNul(untrackedOut));
|
|
764
|
-
return {
|
|
765
|
-
ok: true,
|
|
766
|
-
state: {
|
|
767
|
-
tracked: parseStageEntries(stageOut),
|
|
768
|
-
untracked,
|
|
769
|
-
dirty: uniqueSorted([
|
|
770
|
-
...splitNul(unstagedOut),
|
|
771
|
-
...splitNul(stagedOut),
|
|
772
|
-
...untracked,
|
|
773
|
-
]),
|
|
774
|
-
head,
|
|
775
|
-
},
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
catch (error) {
|
|
779
|
-
if (error instanceof GitCaptureFailure) {
|
|
780
|
-
return { ok: false, status: error.status, detail: error.message };
|
|
781
|
-
}
|
|
782
|
-
return { ok: false, status: "error", detail: String(error) };
|
|
783
|
-
}
|
|
784
|
-
}
|
|
673
|
+
// The Git boundary capture/fingerprint implementation lives in
|
|
674
|
+
// git-boundary.ts (shared with workspace checkpoints).
|
|
785
675
|
// ── Built-in: GitEvidenceProvider ──────────────────────────────────────────
|
|
786
676
|
/** The runtime's resolved state directory, or null when it cannot be observed
|
|
787
677
|
* (configured outside the workspace, or an unresolvable path). */
|
|
@@ -800,111 +690,6 @@ function isInsideDirectory(file, workspace, dir) {
|
|
|
800
690
|
const prefix = dir.endsWith(sep) ? dir : `${dir}${sep}`;
|
|
801
691
|
return absolute === dir || absolute.startsWith(prefix);
|
|
802
692
|
}
|
|
803
|
-
function hashText(text) {
|
|
804
|
-
return createHash("sha256").update(text).digest("hex");
|
|
805
|
-
}
|
|
806
|
-
/** Stream a regular file through SHA-256 without loading it into memory. */
|
|
807
|
-
function hashFileStream(abs, signal) {
|
|
808
|
-
return new Promise((resolveHash, reject) => {
|
|
809
|
-
const hash = createHash("sha256");
|
|
810
|
-
const stream = createReadStream(abs, { highWaterMark: 64 * 1024 });
|
|
811
|
-
let settled = false;
|
|
812
|
-
const cleanup = () => {
|
|
813
|
-
if (signal)
|
|
814
|
-
signal.removeEventListener("abort", onAbort);
|
|
815
|
-
};
|
|
816
|
-
const finish = (error) => {
|
|
817
|
-
if (settled)
|
|
818
|
-
return;
|
|
819
|
-
settled = true;
|
|
820
|
-
cleanup();
|
|
821
|
-
if (error)
|
|
822
|
-
reject(error);
|
|
823
|
-
else
|
|
824
|
-
resolveHash(hash.digest("hex"));
|
|
825
|
-
};
|
|
826
|
-
const onAbort = () => {
|
|
827
|
-
stream.destroy();
|
|
828
|
-
finish(new GitCaptureFailure("aborted", "boundary hashing aborted"));
|
|
829
|
-
};
|
|
830
|
-
if (signal?.aborted) {
|
|
831
|
-
onAbort();
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
835
|
-
stream.on("data", (chunk) => hash.update(chunk));
|
|
836
|
-
stream.on("error", (error) => finish(error));
|
|
837
|
-
stream.on("end", () => finish());
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
/** Fingerprint one boundary path.
|
|
841
|
-
* - symlinks hash their LINK TEXT; the target is never read
|
|
842
|
-
* - regular files stream their content
|
|
843
|
-
* - gitlinks record the index commit (submodule contents are not covered)
|
|
844
|
-
* A per-file read/containment failure is an explicit `unreadable:<mode>`
|
|
845
|
-
* sentinel, not an omitted path. */
|
|
846
|
-
async function boundaryFingerprint(workspace, path, tracked, signal) {
|
|
847
|
-
const abs = resolve(workspace, path);
|
|
848
|
-
let stat;
|
|
849
|
-
try {
|
|
850
|
-
stat = lstatSync(abs);
|
|
851
|
-
}
|
|
852
|
-
catch (error) {
|
|
853
|
-
return error.code === "ENOENT" ? "missing" : "unreadable";
|
|
854
|
-
}
|
|
855
|
-
const mode = stat.mode;
|
|
856
|
-
if (stat.isSymbolicLink()) {
|
|
857
|
-
try {
|
|
858
|
-
// The parent must be contained; the final link target is NOT followed.
|
|
859
|
-
const parent = path.includes("/")
|
|
860
|
-
? path.slice(0, path.lastIndexOf("/")) || "."
|
|
861
|
-
: ".";
|
|
862
|
-
containInWorkspace(workspace, parent);
|
|
863
|
-
return `link:${hashText(readlinkSync(abs))}`;
|
|
864
|
-
}
|
|
865
|
-
catch {
|
|
866
|
-
return `unreadable:${mode}`;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
if (stat.isDirectory()) {
|
|
870
|
-
if (tracked?.mode === "160000")
|
|
871
|
-
return `gitlink:${tracked.object}`;
|
|
872
|
-
return `dir:${mode}`;
|
|
873
|
-
}
|
|
874
|
-
if (!stat.isFile())
|
|
875
|
-
return `other:${mode}`;
|
|
876
|
-
try {
|
|
877
|
-
containInWorkspace(workspace, path);
|
|
878
|
-
const digest = await hashFileStream(abs, signal);
|
|
879
|
-
return `${mode}:${digest}`;
|
|
880
|
-
}
|
|
881
|
-
catch (error) {
|
|
882
|
-
if (error instanceof GitCaptureFailure)
|
|
883
|
-
throw error;
|
|
884
|
-
return `unreadable:${mode}`;
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
/** Hash the complete boundary with bounded concurrency. */
|
|
888
|
-
async function fingerprintBoundary(workspace, entries, signal) {
|
|
889
|
-
const paths = [...entries.keys()].sort();
|
|
890
|
-
const fingerprints = {};
|
|
891
|
-
let next = 0;
|
|
892
|
-
const worker = async () => {
|
|
893
|
-
for (;;) {
|
|
894
|
-
if (signal?.aborted) {
|
|
895
|
-
throw new GitCaptureFailure("aborted", "boundary hashing aborted");
|
|
896
|
-
}
|
|
897
|
-
const index = next++;
|
|
898
|
-
if (index >= paths.length)
|
|
899
|
-
return;
|
|
900
|
-
const path = paths[index];
|
|
901
|
-
fingerprints[path] = await boundaryFingerprint(workspace, path, entries.get(path), signal);
|
|
902
|
-
}
|
|
903
|
-
};
|
|
904
|
-
const workers = Math.min(BOUNDARY_HASH_CONCURRENCY, Math.max(1, paths.length));
|
|
905
|
-
await Promise.all(Array.from({ length: workers }, () => worker()));
|
|
906
|
-
return fingerprints;
|
|
907
|
-
}
|
|
908
693
|
export class GitEvidenceProvider {
|
|
909
694
|
name = "git";
|
|
910
695
|
kind = "git";
|
|
@@ -939,56 +724,48 @@ export class GitEvidenceProvider {
|
|
|
939
724
|
}, budget)
|
|
940
725
|
: undefined;
|
|
941
726
|
try {
|
|
942
|
-
const result = await
|
|
727
|
+
const result = await captureBoundary({
|
|
728
|
+
cwd: workspace,
|
|
729
|
+
signal: controller.signal,
|
|
730
|
+
...(context?.timeoutMs !== undefined ? { timeoutMs: context.timeoutMs } : {}),
|
|
731
|
+
exclude: isRuntimeState,
|
|
732
|
+
...(context?.materializer ? { materializer: context.materializer } : {}),
|
|
733
|
+
...(context?.maxPaths !== undefined ? { maxPaths: context.maxPaths } : {}),
|
|
734
|
+
});
|
|
943
735
|
if (timedOut) {
|
|
944
736
|
return syntheticObservation(this.name, this.kind, phase, startedAt, "timeout", `boundary capture exceeded ${budget}ms`);
|
|
945
737
|
}
|
|
946
738
|
if (!result.ok) {
|
|
947
739
|
return syntheticObservation(this.name, this.kind, phase, startedAt, result.status, result.detail);
|
|
948
740
|
}
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
dirty,
|
|
978
|
-
fingerprints,
|
|
979
|
-
head: result.state.head,
|
|
980
|
-
coverage: "tracked_and_non_ignored_untracked",
|
|
981
|
-
},
|
|
982
|
-
};
|
|
983
|
-
}
|
|
984
|
-
catch (error) {
|
|
985
|
-
const status = timedOut
|
|
986
|
-
? "timeout"
|
|
987
|
-
: error instanceof GitCaptureFailure
|
|
988
|
-
? error.status
|
|
989
|
-
: "error";
|
|
990
|
-
return syntheticObservation(this.name, this.kind, phase, startedAt, status, String(error instanceof Error ? error.message : error));
|
|
991
|
-
}
|
|
741
|
+
const boundary = result.boundary;
|
|
742
|
+
context?.boundarySink?.(boundary);
|
|
743
|
+
return {
|
|
744
|
+
schemaVersion: expectedObservationSchemaVersion("git"),
|
|
745
|
+
providerId: "git",
|
|
746
|
+
kind: "git",
|
|
747
|
+
phase,
|
|
748
|
+
startedAt,
|
|
749
|
+
finishedAt: Date.now(),
|
|
750
|
+
status: "observed",
|
|
751
|
+
// `files` is the dirty display list; the complete boundary lives in
|
|
752
|
+
// data.fingerprints and the round delta is derived from it.
|
|
753
|
+
files: [...boundary.dirty],
|
|
754
|
+
data: {
|
|
755
|
+
dirty: [...boundary.dirty],
|
|
756
|
+
fingerprints: boundary.fingerprints,
|
|
757
|
+
...(boundary.head ? { head: boundary.head } : {}),
|
|
758
|
+
coverage: "tracked_and_non_ignored_untracked",
|
|
759
|
+
},
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
catch (error) {
|
|
763
|
+
const status = timedOut
|
|
764
|
+
? "timeout"
|
|
765
|
+
: error instanceof GitCaptureFailure
|
|
766
|
+
? error.status
|
|
767
|
+
: "error";
|
|
768
|
+
return syntheticObservation(this.name, this.kind, phase, startedAt, status, String(error instanceof Error ? error.message : error));
|
|
992
769
|
}
|
|
993
770
|
finally {
|
|
994
771
|
if (timer)
|
|
@@ -998,6 +775,78 @@ export class GitEvidenceProvider {
|
|
|
998
775
|
}
|
|
999
776
|
}
|
|
1000
777
|
registerEvidenceProvider("git", () => new GitEvidenceProvider());
|
|
778
|
+
/** The Git fact descriptor is derived from the PERSISTED observation, not from
|
|
779
|
+
* a code constant: the scope is the boundary coverage the observation
|
|
780
|
+
* declared and the version is its schema version, so a historical
|
|
781
|
+
* observation with different evidence semantics naturally stops matching the
|
|
782
|
+
* rule instead of being retroactively rewritten by future code. */
|
|
783
|
+
function gitProgressCoverage(observation) {
|
|
784
|
+
if (observation?.kind === "git") {
|
|
785
|
+
const scope = typeof observation.data.coverage === "string" &&
|
|
786
|
+
observation.data.coverage.length > 0
|
|
787
|
+
? observation.data.coverage
|
|
788
|
+
: GIT_WORKSPACE_COVERAGE.scope;
|
|
789
|
+
const version = Number.isInteger(observation.schemaVersion) &&
|
|
790
|
+
observation.schemaVersion >= 1
|
|
791
|
+
? observation.schemaVersion
|
|
792
|
+
: GIT_WORKSPACE_COVERAGE.version;
|
|
793
|
+
return {
|
|
794
|
+
domain: GIT_WORKSPACE_COVERAGE.domain,
|
|
795
|
+
object: GIT_WORKSPACE_COVERAGE.object,
|
|
796
|
+
scope,
|
|
797
|
+
version,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
return { ...GIT_WORKSPACE_COVERAGE };
|
|
801
|
+
}
|
|
802
|
+
function deltaProgressFact(observation, beforeObservation, afterObservation, changedFiles, ambiguous) {
|
|
803
|
+
if (observation.kind === "git") {
|
|
804
|
+
const coverage = gitProgressCoverage(afterObservation ?? beforeObservation);
|
|
805
|
+
const observable = !ambiguous &&
|
|
806
|
+
beforeObservation?.kind === "git" &&
|
|
807
|
+
afterObservation?.kind === "git" &&
|
|
808
|
+
beforeObservation.status === "observed" &&
|
|
809
|
+
afterObservation.status === "observed" &&
|
|
810
|
+
changedFiles !== null;
|
|
811
|
+
if (!observable) {
|
|
812
|
+
return {
|
|
813
|
+
kind: "git",
|
|
814
|
+
coverage,
|
|
815
|
+
state: "unknown",
|
|
816
|
+
complete: false,
|
|
817
|
+
detail: "the Git boundary pair is not comparable",
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
const signal = gitMotionSignal({
|
|
821
|
+
changedFiles,
|
|
822
|
+
headBefore: observedGitHead(beforeObservation),
|
|
823
|
+
headAfter: observedGitHead(afterObservation),
|
|
824
|
+
});
|
|
825
|
+
if (signal === null) {
|
|
826
|
+
return {
|
|
827
|
+
kind: "git",
|
|
828
|
+
coverage,
|
|
829
|
+
state: "unknown",
|
|
830
|
+
complete: true,
|
|
831
|
+
detail: "the Git change set is known but HEAD movement is not comparable",
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
kind: "git",
|
|
836
|
+
coverage,
|
|
837
|
+
state: signal ? "changed" : "unchanged",
|
|
838
|
+
complete: true,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
// Custom observations are reports. They carry no progress fact, so they can
|
|
842
|
+
// never influence stall or prompt freshness.
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
/** Stable display key for a coverage descriptor — derived only, never
|
|
846
|
+
* persisted, never used as an identity substitute for exact comparison. */
|
|
847
|
+
export function coverageKey(coverage) {
|
|
848
|
+
return `${coverage.domain}/${coverage.object}/${coverage.scope}@${coverage.version}`;
|
|
849
|
+
}
|
|
1001
850
|
const DELTA_KIND_ORDER = {
|
|
1002
851
|
git: 0,
|
|
1003
852
|
command: 1,
|
|
@@ -1076,6 +925,7 @@ export function deriveObservationDeltas(before, after) {
|
|
|
1076
925
|
before: beforeObservation,
|
|
1077
926
|
after: afterObservation,
|
|
1078
927
|
changedFiles,
|
|
928
|
+
progress: deltaProgressFact(observation, beforeObservation, afterObservation, changedFiles, ambiguous),
|
|
1079
929
|
};
|
|
1080
930
|
});
|
|
1081
931
|
return deltas.sort((left, right) => DELTA_KIND_ORDER[left.kind] - DELTA_KIND_ORDER[right.kind] ||
|