@lazyingart/agintiflow 0.20.299 → 0.20.301

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.
@@ -6,7 +6,7 @@ AgInTiFlow separates visual understanding, web search, and wrapper advice so eac
6
6
 
7
7
  | Tool | Purpose | Evidence |
8
8
  | --- | --- | --- |
9
- | `read_image` | Read workspace screenshots, plots, scanned text, diagrams, or allowed remote image URLs. | Saves JSON and Markdown reports under `artifacts/perception/`, records image hashes, and sends the Markdown report to the canvas when used by an agent run. |
9
+ | `read_image` | Read workspace screenshots, plots, scanned text, diagrams, or allowed remote image URLs. | Saves canonical JSON/Markdown evidence in the session artifact store, keeps the workspace-relative canvas copy under ignored `.aginti/artifacts/perception/`, and records image hashes. |
10
10
  | `web_search` | Cheap raw search snippets or a bounded multi-provider ensemble. | Returns compact titles, canonical URLs, snippets, provider attempts, and per-provider discovery evidence. |
11
11
  | `read_web_page` | Read one exact public source instead of relying on a snippet. | Returns bounded article/PDF text, metadata, canonical URL, relevant passages, retrieval time, and content hash. |
12
12
  | `web_research` | Sourced research unit for current or external information. | Saves `artifacts/research/*-web-research.json` with query, mode, source list, and answer. |
@@ -18,6 +18,7 @@ AgInTiFlow separates visual understanding, web search, and wrapper advice so eac
18
18
  - In a LocalLLM session, `read_image provider=auto` sends bounded image data to the loopback LocalLLM Chat Completions endpoint with `AGINTI_LOCALLLM_VISION_MODEL` or `localllm-vision-xl`. It does not try OpenAI or a wrapper after a local failure.
19
19
  - In an OpenAI session, `read_image provider=auto` uses OpenAI Responses vision. A non-OpenAI session must set `allowHostedImagePerception=true` before `provider=openai` is accepted. `OPENAI_API_KEY` by itself grants no permission.
20
20
  - Codex image reading requires both `provider=codex` and explicitly enabled wrapper tools. It is not an automatic fallback.
21
+ - Visual verification never creates a task-owned `artifacts/perception/` tree. Its workspace-relative Markdown copy stays under ignored `.aginti/` storage so a successful review cannot dirty an otherwise clean repository.
21
22
  - OpenAI image reading defaults to `AGINTI_PERCEPTION_MODEL=gpt-5.4-mini` and `AGINTI_PERCEPTION_REASONING=medium`, with fallback models remaining inside the same explicitly selected OpenAI provider.
22
23
  - `web_research` defaults to lightweight snippet mode. The active LocalLLM can synthesize those returned snippets and sources during the next agent step without a second provider call.
23
24
  - `web_search provider=auto` falls back from DuckDuckGo HTML to Bing RSS. `provider=multi` merges both indexes and adds Brave only when explicitly configured. Standard/deep research defaults to this ensemble; quick research stays on auto.
@@ -132,6 +132,10 @@ This starts interactive chat with Docker workspace mode, file tools, shell tools
132
132
 
133
133
  Docker workspace mode also mounts common host data roots, such as the user's home parent, read-only at their original absolute paths. This lets a task in `/home/lachlan/ProjectsLFS/aginti-test` inspect a sibling dataset such as `/home/lachlan/ProjectsLFS/ProteinStructure` without switching to host-write mode. Outputs and edits still belong under `/workspace` unless the user explicitly approves stronger host permissions. Set `AGINTI_DOCKER_READONLY_HOST_MOUNTS=/path/a:/path/b` to override the read-only mounts, or set it to `off` to disable them.
134
134
 
135
+ For a linked Git worktree, bounded Git mutation commands such as the runtime's path-enumerated `git add` plus `git commit` also receive a writable mount for the validated shared Git common directory. The mount is not added to ordinary shell commands or read-only Git inspection. This lets Docker workspace mode commit the current worktree without granting broad host-write access.
136
+
137
+ Outstanding exact project commands always run before Git completion. A pending producer, test, or user-declared validation command cannot be preempted by a commit merely because the current artifacts already exist.
138
+
135
139
  Direct trusted host mode:
136
140
 
137
141
  ```bash
@@ -56,6 +56,45 @@ in `evidence_json` and `events`.
56
56
 
57
57
  ## Current Campaign Findings
58
58
 
59
+ ### Established media-chain reuse and permission boundaries
60
+
61
+ `media-chain-readiness-020` passed after reconciling a stale machine-ledger
62
+ `running` row with the retained session's terminal `session.finished` event.
63
+ AgInTi inspected the existing Musia, LabCanvas, LALACHAN, Xiaoyunque, and
64
+ LazyEdit entry points and produced `media-routine-readiness.md` without
65
+ generating media, starting or focusing a browser, uploading, copying to
66
+ Nutstore, creating a remote job, or publishing.
67
+
68
+ The independent acceptance contract verified every required source-grounded
69
+ entry, 16 read-only command results, zero forbidden external-action tools, and
70
+ zero long jobs. The resulting contract keeps four permissions independent:
71
+ reviewed-song generation, music-video generation, LazyEdit processing, and
72
+ public platform publication. An earlier stage or old context cannot authorize a
73
+ later irreversible stage.
74
+
75
+ ### Retained dependencies, linked-worktree commits, and clean visual evidence
76
+
77
+ `data-analysis-retained-dependencies-060` resumed the original DeepSeek session
78
+ after an imperfect data-analysis repair had narrowed its context too far. The
79
+ agent retained the immutable configuration and CSV inputs referenced by the
80
+ mutable Python source, regenerated every required output, ran the canonical
81
+ producer and all four unit tests, inspected the 900 x 600 plot, and committed
82
+ target revision `55cadcd` from Docker workspace mode. Independent verification
83
+ confirmed 11 raw rows, one duplicate removal, two invalid-signal removals,
84
+ eight clean rows, the expected condition means, valid CSV/JSON/Markdown/PNG
85
+ artifacts, and a clean target worktree.
86
+
87
+ The run established three reusable runtime contracts:
88
+
89
+ - Failed-test recovery keeps bounded, previously read immutable dependencies
90
+ when the mutable source names them, while mutation scope remains limited to
91
+ the canonical repair paths.
92
+ - An outstanding exact producer or validation command runs before artifact Git
93
+ completion. In a linked worktree, only bounded Git mutation commands receive
94
+ a writable mount for the validated shared Git common directory.
95
+ - `read_image` stores its task-relative review copy under ignored `.aginti/`
96
+ runtime storage, so visual verification cannot dirty a completed repository.
97
+
59
98
  ### Data analysis retained-state recovery
60
99
 
61
100
  `data-analysis-local-011` passed after reusable runtime fixes. AgInTi completed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.299",
3
+ "version": "0.20.301",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
6
6
  "license": "Apache-2.0",
@@ -7,6 +7,7 @@ import path from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { promisify } from "node:util";
9
9
  import {
10
+ dockerWritableGitMounts,
10
11
  dockerWorkspaceAliasMounts,
11
12
  ensureDockerGitIdentity,
12
13
  } from "../src/docker-sandbox.js";
@@ -52,6 +53,47 @@ try {
52
53
  assert.equal(storedEmail.stdout.trim(), "aginti-test@example.invalid");
53
54
  assert.equal((await fs.stat(identityConfig)).mode & 0o777, 0o600, "Docker author config should be private");
54
55
 
56
+ const worktreeRoot = path.join(tempRoot, "worktree-root");
57
+ const linkedWorktree = path.join(tempRoot, "linked-worktree");
58
+ await execFileAsync("git", ["init", worktreeRoot]);
59
+ await execFileAsync("git", ["-C", worktreeRoot, "config", "user.name", "AgInTi Test Author"]);
60
+ await execFileAsync("git", ["-C", worktreeRoot, "config", "user.email", "aginti-test@example.invalid"]);
61
+ await fs.writeFile(path.join(worktreeRoot, "seed.txt"), "seed\n", "utf8");
62
+ await execFileAsync("git", ["-C", worktreeRoot, "add", "seed.txt"]);
63
+ await execFileAsync("git", ["-C", worktreeRoot, "commit", "-m", "seed"]);
64
+ await execFileAsync("git", ["-C", worktreeRoot, "worktree", "add", "-b", "linked-test", linkedWorktree]);
65
+ const linkedCommonDir = (await execFileAsync(
66
+ "git",
67
+ ["-C", linkedWorktree, "rev-parse", "--path-format=absolute", "--git-common-dir"]
68
+ )).stdout.trim();
69
+ assert.deepEqual(
70
+ dockerWritableGitMounts(
71
+ "git add -- changed.txt && git commit -m 'linked worktree change'",
72
+ { commandCwd: linkedWorktree, sandboxMode: "docker-workspace" },
73
+ { gitOnly: true }
74
+ ),
75
+ [linkedCommonDir],
76
+ "Docker Git mutation did not mount validated linked-worktree metadata writable"
77
+ );
78
+ assert.deepEqual(
79
+ dockerWritableGitMounts(
80
+ "git status --short",
81
+ { commandCwd: linkedWorktree, sandboxMode: "docker-workspace" },
82
+ { gitOnly: true }
83
+ ),
84
+ [],
85
+ "read-only Git inspection received an unnecessary writable metadata mount"
86
+ );
87
+ assert.deepEqual(
88
+ dockerWritableGitMounts(
89
+ "git add -- seed.txt",
90
+ { commandCwd: worktreeRoot, sandboxMode: "docker-workspace" },
91
+ { gitOnly: true }
92
+ ),
93
+ [],
94
+ "ordinary repositories should rely on the existing writable workspace mount"
95
+ );
96
+
55
97
  const { stdout } = await execFileAsync(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"), "docker", "status", "--json", "--cwd", tempRoot], {
56
98
  cwd: tempRoot,
57
99
  env: {
@@ -572,6 +572,98 @@ try {
572
572
  duplicateFocus?.duplicateDeclarations?.[0]?.name === "status_service",
573
573
  "duplicate production declarations were not promoted into a focused failed-test repair"
574
574
  );
575
+ const dependencyRecoveryWorkspace = path.join(tempRoot, "dependency-recovery-workspace");
576
+ await fs.mkdir(path.join(dependencyRecoveryWorkspace, "config"), { recursive: true });
577
+ await fs.mkdir(path.join(dependencyRecoveryWorkspace, "raw"), { recursive: true });
578
+ await fs.mkdir(path.join(dependencyRecoveryWorkspace, "tests"), { recursive: true });
579
+ await fs.writeFile(
580
+ path.join(dependencyRecoveryWorkspace, "analysis.py"),
581
+ [
582
+ "from pathlib import Path",
583
+ "import json",
584
+ "",
585
+ "ROOT = Path(__file__).resolve().parent",
586
+ "config = json.loads((ROOT / 'config' / 'experiment.json').read_text())",
587
+ "for source_file in ('run_a.csv', 'run_b.csv'):",
588
+ " print((ROOT / 'raw' / source_file).read_text())",
589
+ "",
590
+ ].join("\n"),
591
+ "utf8"
592
+ );
593
+ await fs.writeFile(
594
+ path.join(dependencyRecoveryWorkspace, "tests", "test_analysis.py"),
595
+ "from analysis import config\n",
596
+ "utf8"
597
+ );
598
+ await fs.writeFile(
599
+ path.join(dependencyRecoveryWorkspace, "config", "experiment.json"),
600
+ '{"offset":10}\n',
601
+ "utf8"
602
+ );
603
+ await fs.writeFile(
604
+ path.join(dependencyRecoveryWorkspace, "raw", "run_a.csv"),
605
+ "sample,signal\nA,100\n",
606
+ "utf8"
607
+ );
608
+ await fs.writeFile(
609
+ path.join(dependencyRecoveryWorkspace, "raw", "run_b.csv"),
610
+ "sample,signal\nB,90\n",
611
+ "utf8"
612
+ );
613
+ await fs.writeFile(
614
+ path.join(dependencyRecoveryWorkspace, "raw", "unrelated.csv"),
615
+ "private,unrelated\nno,1\n",
616
+ "utf8"
617
+ );
618
+ const dependencyRecoveryState = {
619
+ meta: {
620
+ projectVerification: {
621
+ mutationRevision: 2,
622
+ lastMutation: {
623
+ revision: 2,
624
+ toolName: "apply_patch",
625
+ paths: ["analysis.py"],
626
+ },
627
+ discoveredTests: ["tests/test_analysis.py"],
628
+ testRuns: [{
629
+ command: "python analysis.py",
630
+ mutationRevision: 2,
631
+ passed: false,
632
+ failureEvidenceVersion: 2,
633
+ failureSignature: "source-dependency-schema-mismatch",
634
+ failureSummary: "File \"analysis.py\", line 7, in <module>: KeyError: signal_mv",
635
+ }],
636
+ },
637
+ toolLoop: {
638
+ recent: [
639
+ "config/experiment.json",
640
+ "raw/run_a.csv",
641
+ "raw/run_b.csv",
642
+ "raw/unrelated.csv",
643
+ ].map((readPath) => ({
644
+ toolName: "read_file",
645
+ path: readPath,
646
+ ok: true,
647
+ blocked: false,
648
+ })),
649
+ },
650
+ },
651
+ };
652
+ const dependencyRecoveryPacket = await buildFailedTestRecoveryPacket(
653
+ { commandCwd: dependencyRecoveryWorkspace },
654
+ dependencyRecoveryState
655
+ );
656
+ assert(
657
+ [
658
+ "analysis.py",
659
+ "config/experiment.json",
660
+ "raw/run_a.csv",
661
+ "raw/run_b.csv",
662
+ ].every((item) => dependencyRecoveryPacket.paths.includes(item)) &&
663
+ !dependencyRecoveryPacket.paths.includes("raw/unrelated.csv") &&
664
+ JSON.stringify(dependencyRecoveryPacket.repairPaths) === JSON.stringify(["analysis.py"]),
665
+ `failed-test recovery dropped previously read immutable dependencies or widened mutation scope: ${JSON.stringify(dependencyRecoveryPacket)}`
666
+ );
575
667
  const baselineWorkspace = path.join(tempRoot, "baseline-recovery-workspace");
576
668
  await fs.mkdir(baselineWorkspace, { recursive: true });
577
669
  const trackedBaselineSource = [
@@ -31,14 +31,23 @@ import {
31
31
  } from "../src/scs-controller.js";
32
32
  import {
33
33
  buildScsEvidenceLedger,
34
+ buildTmuxGitIntent,
34
35
  deriveScsTaskContract,
35
36
  evaluateScsEvidence,
36
37
  gitActionsSatisfyContract,
37
38
  inferSuccessfulGitActionsFromCommandResult,
39
+ inferSuccessfulGitActionsFromTmuxCapture,
40
+ reconcileTmuxGitEvidenceEvents,
38
41
  } from "../src/scs-evidence.js";
39
42
  import { resolveRuntimeConfig } from "../src/config.js";
40
43
  import { classifyGoalIntent, isDirectAnswerIntent } from "../src/goal-intent.js";
41
44
  import { languageWriterDefaults } from "../src/writing-specialist.js";
45
+ import {
46
+ attachVerifiedTmuxGitEvidence,
47
+ recordDurableEvidenceCategories,
48
+ rememberTmuxGitIntent,
49
+ sanitizeToolResult,
50
+ } from "../src/agent-runner.js";
42
51
 
43
52
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
44
53
  for (const key of [
@@ -1117,6 +1126,195 @@ assert(
1117
1126
  !committedEvaluation.missing.some((item) => item.category === "git"),
1118
1127
  "a successful git commit did not satisfy the explicit git requirement"
1119
1128
  );
1129
+ const tmuxCommitCommand = [
1130
+ 'echo "===AGINTI_REVIEW_COMMIT_START==="',
1131
+ 'git add README.md && git commit -m "finish review"',
1132
+ 'echo "===COMMIT_EXIT:$?==="',
1133
+ ].join(" ; ");
1134
+ const tmuxCommitIntent = buildTmuxGitIntent(tmuxCommitCommand);
1135
+ assert(
1136
+ tmuxCommitIntent?.actions.includes("commit") &&
1137
+ tmuxCommitIntent.markers.includes("===AGINTI_REVIEW_COMMIT_START==="),
1138
+ "a marked workspace tmux commit command did not retain bounded Git intent"
1139
+ );
1140
+ const tmuxCommitCapture = {
1141
+ ok: true,
1142
+ toolName: "tmux_capture_pane",
1143
+ target: "review:0.0",
1144
+ content: [
1145
+ "===AGINTI_REVIEW_COMMIT_START===",
1146
+ "[review abc1234] finish review",
1147
+ " 1 file changed, 2 insertions(+)",
1148
+ "===COMMIT_EXIT:0===",
1149
+ ].join("\n"),
1150
+ };
1151
+ const verifiedTmuxCommit = inferSuccessfulGitActionsFromTmuxCapture(
1152
+ tmuxCommitIntent,
1153
+ tmuxCommitCapture
1154
+ );
1155
+ assert(
1156
+ verifiedTmuxCommit.length === 1 && verifiedTmuxCommit[0] === "commit",
1157
+ "a fresh marked tmux capture did not prove its successful Git commit"
1158
+ );
1159
+ assert(
1160
+ inferSuccessfulGitActionsFromTmuxCapture(tmuxCommitIntent, {
1161
+ ...tmuxCommitCapture,
1162
+ content: tmuxCommitCapture.content.replace("COMMIT_EXIT:0", "COMMIT_EXIT:1"),
1163
+ }).length === 0,
1164
+ "a nonzero tmux commit exit marker was accepted as Git evidence"
1165
+ );
1166
+ assert(
1167
+ inferSuccessfulGitActionsFromTmuxCapture(tmuxCommitIntent, {
1168
+ ...tmuxCommitCapture,
1169
+ content: tmuxCommitCapture.content.replace("AGINTI_REVIEW_COMMIT_START", "STALE_COMMIT_START"),
1170
+ }).length === 0,
1171
+ "a tmux capture without the current command marker was accepted as fresh Git evidence"
1172
+ );
1173
+ const tmuxEvidenceState = {
1174
+ meta: {
1175
+ goalContract: { revision: 2 },
1176
+ projectVerification: { mutationRevision: 3 },
1177
+ },
1178
+ };
1179
+ const tmuxSendResult = {
1180
+ ok: true,
1181
+ toolName: "tmux_send_keys",
1182
+ target: "review:0.0",
1183
+ sentTextSha256: "a".repeat(64),
1184
+ };
1185
+ rememberTmuxGitIntent(
1186
+ tmuxEvidenceState,
1187
+ { text: tmuxCommitCommand },
1188
+ tmuxSendResult
1189
+ );
1190
+ const trackedTmuxCapture = { ...tmuxCommitCapture };
1191
+ assert(
1192
+ attachVerifiedTmuxGitEvidence(tmuxEvidenceState, trackedTmuxCapture).includes("commit"),
1193
+ "the same-pane tmux capture did not close the pending Git intent"
1194
+ );
1195
+ recordDurableEvidenceCategories(tmuxEvidenceState, trackedTmuxCapture);
1196
+ assert(
1197
+ tmuxEvidenceState.meta.durableGitEvidence.some(
1198
+ (item) =>
1199
+ item.action === "commit" &&
1200
+ item.goalRevision === 2 &&
1201
+ item.mutationRevision === 3
1202
+ ),
1203
+ "verified tmux commit evidence was not retained at the active goal and mutation revision"
1204
+ );
1205
+ assert(
1206
+ !tmuxEvidenceState.meta.pendingTmuxGitEvidence["review:0.0"],
1207
+ "verified tmux Git intent remained pending after same-pane capture"
1208
+ );
1209
+ const tmuxCommitLedger = buildScsEvidenceLedger({
1210
+ context: {
1211
+ events: [
1212
+ {
1213
+ type: "tool.completed",
1214
+ data: {
1215
+ ...tmuxCommitCapture,
1216
+ verifiedGitActions: verifiedTmuxCommit,
1217
+ verifiedGitGoalRevision: 1,
1218
+ verifiedGitMutationRevision: 1,
1219
+ },
1220
+ },
1221
+ ],
1222
+ },
1223
+ });
1224
+ assert(
1225
+ evaluateScsEvidence(commitContract, tmuxCommitLedger).missingGitActions.length === 0,
1226
+ "verified tmux commit evidence did not satisfy the completion contract"
1227
+ );
1228
+ const historicalTmuxEvents = [
1229
+ { type: "goal.updated", data: { revision: 2 } },
1230
+ {
1231
+ type: "tool.completed",
1232
+ data: {
1233
+ ok: true,
1234
+ toolName: "run_command",
1235
+ goalRevision: 2,
1236
+ projectMutationRevision: 3,
1237
+ args: { command: "git status --short" },
1238
+ exitCode: 0,
1239
+ stdout: " M README.md",
1240
+ },
1241
+ },
1242
+ {
1243
+ type: "tool.started",
1244
+ data: {
1245
+ toolName: "tmux_send_keys",
1246
+ args: { target: "review:0.0", text: tmuxCommitCommand },
1247
+ },
1248
+ },
1249
+ {
1250
+ type: "tool.completed",
1251
+ data: {
1252
+ ...tmuxSendResult,
1253
+ target: "review:0.0",
1254
+ },
1255
+ },
1256
+ {
1257
+ type: "tool.completed",
1258
+ data: {
1259
+ ...tmuxCommitCapture,
1260
+ content: undefined,
1261
+ contentPreview: tmuxCommitCapture.content,
1262
+ },
1263
+ },
1264
+ ];
1265
+ const reconciledHistoricalEvents = reconcileTmuxGitEvidenceEvents(
1266
+ historicalTmuxEvents
1267
+ );
1268
+ assert(
1269
+ reconciledHistoricalEvents.at(-1)?.data?.verifiedGitActions?.includes("commit") &&
1270
+ reconciledHistoricalEvents.at(-1)?.data?.verifiedGitGoalRevision === 2 &&
1271
+ reconciledHistoricalEvents.at(-1)?.data?.verifiedGitMutationRevision === 3,
1272
+ "restart reconciliation did not recover revision-scoped tmux commit evidence"
1273
+ );
1274
+ assert(
1275
+ evaluateScsEvidence(
1276
+ commitContract,
1277
+ buildScsEvidenceLedger({ context: { events: historicalTmuxEvents } })
1278
+ ).missingGitActions.length === 0,
1279
+ "historical marked tmux commit events did not satisfy the completion contract"
1280
+ );
1281
+ for (const invalidEvents of [
1282
+ historicalTmuxEvents.map((event, index) =>
1283
+ index === 3 ? { ...event, type: "tool.failed", data: { ...event.data, ok: false } } : event
1284
+ ),
1285
+ historicalTmuxEvents.map((event, index) =>
1286
+ index === 4 ? { ...event, data: { ...event.data, target: "other:0.0" } } : event
1287
+ ),
1288
+ historicalTmuxEvents.map((event, index) =>
1289
+ index === 4
1290
+ ? {
1291
+ ...event,
1292
+ data: {
1293
+ ...event.data,
1294
+ contentPreview: event.data.contentPreview.replace("COMMIT_EXIT:0", "COMMIT_EXIT:1"),
1295
+ },
1296
+ }
1297
+ : event
1298
+ ),
1299
+ ]) {
1300
+ assert(
1301
+ !reconcileTmuxGitEvidenceEvents(invalidEvents).at(-1)?.data?.verifiedGitActions,
1302
+ "failed, cross-pane, or nonzero historical tmux evidence was accepted"
1303
+ );
1304
+ }
1305
+ const sanitizedLongTmuxCapture = sanitizeToolResult({
1306
+ ...tmuxCommitCapture,
1307
+ content: `${"old scrollback\n".repeat(200)}${tmuxCommitCapture.content}`,
1308
+ });
1309
+ assert(
1310
+ sanitizedLongTmuxCapture.contentTruncated === true &&
1311
+ sanitizedLongTmuxCapture.contentTail.includes("===COMMIT_EXIT:0===") &&
1312
+ inferSuccessfulGitActionsFromTmuxCapture(
1313
+ tmuxCommitIntent,
1314
+ sanitizedLongTmuxCapture
1315
+ ).includes("commit"),
1316
+ "truncated tmux evidence did not preserve and verify its bounded content tail"
1317
+ );
1120
1318
  const recoveredCommitActions = inferSuccessfulGitActionsFromCommandResult({
1121
1319
  ok: true,
1122
1320
  exitCode: 0,
@@ -343,6 +343,10 @@ async function main() {
343
343
  assert(image.artifactPath, "read_image did not persist a perception artifact");
344
344
  assert(image.markdownArtifactPath, "read_image did not persist a Markdown perception artifact");
345
345
  assert(image.markdownPath, "read_image did not persist a workspace Markdown report");
346
+ assert(
347
+ image.markdownPath.startsWith(".aginti/artifacts/perception/"),
348
+ `read_image dirtied the task-owned artifact tree: ${image.markdownPath}`
349
+ );
346
350
  assert(
347
351
  path.basename(image.markdownPath).includes("tiny-image-analysis"),
348
352
  `read_image emitted a generic artifact filename: ${image.markdownPath}`
@@ -1584,6 +1584,30 @@ assertStrict.deepEqual(
1584
1584
  "task-owned Git completion delegated path selection back to the model"
1585
1585
  );
1586
1586
 
1587
+ const requiredCommandBeforeCommitTools = selectProgressiveTools(allTools, {
1588
+ config: {
1589
+ provider: "localllm",
1590
+ artifactValidationPhase: true,
1591
+ artifactValidationNeedsCommand: true,
1592
+ requiredProjectCommandPending: true,
1593
+ requiredProjectCommand: "python analysis.py",
1594
+ artifactValidationPendingGitActions: ["commit"],
1595
+ artifactValidationCommitPaths: ["analysis.py", "AGINTI.md"],
1596
+ },
1597
+ goal: "Regenerate the outputs, validate them, and commit the intended repair.",
1598
+ profile: "data",
1599
+ });
1600
+ sameNames(
1601
+ requiredCommandBeforeCommitTools,
1602
+ ["run_command", "finish"],
1603
+ "artifact Git completion preempted an outstanding exact project command"
1604
+ );
1605
+ assertStrict.deepEqual(
1606
+ requiredCommandBeforeCommitTools[0].function.parameters.properties.command.enum,
1607
+ ["python analysis.py"],
1608
+ "artifact recovery exposed an open-ended command instead of the retained requirement"
1609
+ );
1610
+
1587
1611
  const resumedArtifactRuntime = nextStepRuntimeConfig(
1588
1612
  {
1589
1613
  goal: "Perform a visual screenshot inspection of the generated plot and commit the intentional changes.",
@@ -147,9 +147,11 @@ import {
147
147
  finishResultClaimsBlocker,
148
148
  finishResultClaimsIncompleteWork,
149
149
  hasScsBlockerEvidence,
150
+ buildTmuxGitIntent,
150
151
  inferAuthoritativeReadOnlyRoutine,
151
152
  inferGitActionsFromCommand,
152
153
  inferSuccessfulGitActionsFromCommandResult,
154
+ inferSuccessfulGitActionsFromTmuxCapture,
153
155
  isResponseOnlyEvidenceScope,
154
156
  successfulGitCommitProvesFileMutation,
155
157
  isObservationalGitAction,
@@ -8495,7 +8497,7 @@ export function taskForbidsExternalValidatorSourceRead(config = {}, state = {})
8495
8497
  function recoveryEvidenceDependencies(
8496
8498
  sourcePath = "",
8497
8499
  content = "",
8498
- { commandCwd = "" } = {}
8500
+ { commandCwd = "", knownReadPaths = [] } = {}
8499
8501
  ) {
8500
8502
  const explicitDependencies = [];
8501
8503
  const importCandidates = [];
@@ -8531,7 +8533,7 @@ function recoveryEvidenceDependencies(
8531
8533
  append(explicitDependencies, path.posix.normalize(raw));
8532
8534
  }
8533
8535
  for (const match of String(content || "").matchAll(
8534
- /^\s*[A-Za-z_][A-Za-z0-9_]*\s*=\s*[A-Za-z_][A-Za-z0-9_]*(?<segments>(?:\s*\/\s*["'][^"'\r\n]+["']){1,8})\s*$/gm
8536
+ /\b[A-Za-z_][A-Za-z0-9_]*(?<segments>(?:\s*\/\s*["'][^"'\r\n]+["']){1,8})/g
8535
8537
  )) {
8536
8538
  const segments = [...String(match.groups?.segments || "").matchAll(/["']([^"'\r\n]+)["']/g)]
8537
8539
  .map((segment) => String(segment[1] || "").trim())
@@ -8540,6 +8542,22 @@ function recoveryEvidenceDependencies(
8540
8542
  append(explicitDependencies, path.posix.normalize(segments.join("/")));
8541
8543
  }
8542
8544
  }
8545
+ const sourceText = String(content || "");
8546
+ for (const item of Array.isArray(knownReadPaths) ? knownReadPaths : []) {
8547
+ const candidate = safeRecoveryEvidencePath(item);
8548
+ if (!candidate || safeTaskOwnedCommitPath(candidate) !== candidate) continue;
8549
+ const basename = path.posix.basename(candidate);
8550
+ const parentBasename = path.posix.basename(path.posix.dirname(candidate));
8551
+ if (!basename || !sourceText.includes(basename)) continue;
8552
+ if (
8553
+ parentBasename &&
8554
+ parentBasename !== "." &&
8555
+ !sourceText.includes(parentBasename)
8556
+ ) {
8557
+ continue;
8558
+ }
8559
+ append(explicitDependencies, candidate);
8560
+ }
8543
8561
  if (path.posix.extname(sourcePath).toLowerCase() === ".java") {
8544
8562
  const normalizedSource = String(sourcePath || "").replace(/\\/g, "/");
8545
8563
  const packageMatch = String(content || "").match(
@@ -8960,6 +8978,19 @@ export async function buildFailedTestRecoveryPacket(config = {}, state = {}) {
8960
8978
  state,
8961
8979
  testRun
8962
8980
  );
8981
+ const knownReadPaths = (Array.isArray(state.meta?.toolLoop?.recent)
8982
+ ? state.meta.toolLoop.recent
8983
+ : [])
8984
+ .filter(
8985
+ (entry) =>
8986
+ entry?.ok === true &&
8987
+ entry?.blocked !== true &&
8988
+ String(entry?.toolName || "") === "read_file"
8989
+ )
8990
+ .map((entry) => safeRecoveryEvidencePath(toolLoopReadEvidencePath(entry) || entry?.path))
8991
+ .filter((item) => item && safeTaskOwnedCommitPath(item) === item)
8992
+ .filter((item, index, items) => items.indexOf(item) === index)
8993
+ .slice(-24);
8963
8994
  const queue = [
8964
8995
  ...failureMentionedPaths.filter(failedTestPathIsTestEvidence),
8965
8996
  ...(Array.isArray(verification.discoveredTests) ? verification.discoveredTests : []),
@@ -9536,7 +9567,10 @@ export async function buildFailedTestRecoveryPacket(config = {}, state = {}) {
9536
9567
  );
9537
9568
  }
9538
9569
  }
9539
- for (const dependency of recoveryEvidenceDependencies(relativePath, raw, { commandCwd })) {
9570
+ for (const dependency of recoveryEvidenceDependencies(relativePath, raw, {
9571
+ commandCwd,
9572
+ knownReadPaths,
9573
+ })) {
9540
9574
  if (!seen.has(dependency) && !queue.includes(dependency)) queue.push(dependency);
9541
9575
  }
9542
9576
  }
@@ -9756,6 +9790,9 @@ export function sanitizeToolResult(result) {
9756
9790
  safeResult.contentTruncated = false;
9757
9791
  } else {
9758
9792
  safeResult.contentPreview = safeResult.content.slice(0, TOOL_RESULT_CONTENT_PREVIEW_CHARS);
9793
+ if (safeResult.toolName === "tmux_capture_pane") {
9794
+ safeResult.contentTail = safeResult.content.slice(-TOOL_RESULT_CONTENT_PREVIEW_CHARS);
9795
+ }
9759
9796
  safeResult.contentTruncated = true;
9760
9797
  delete safeResult.content;
9761
9798
  }
@@ -13746,7 +13783,55 @@ export function mergeDurableGitEvidence(
13746
13783
  return [...merged.values()].slice(-Math.max(1, Number(limit || 40)));
13747
13784
  }
13748
13785
 
13749
- function recordDurableEvidenceCategories(state = {}, toolResult = {}) {
13786
+ export function rememberTmuxGitIntent(state = {}, args = {}, toolResult = {}) {
13787
+ const gitIntent = toolResult?.ok ? buildTmuxGitIntent(args.text || "") : null;
13788
+ if (!gitIntent || !toolResult?.target) return null;
13789
+ state.meta = state.meta || {};
13790
+ const pendingByTarget = state.meta.pendingTmuxGitEvidence || {};
13791
+ const pending = {
13792
+ ...gitIntent,
13793
+ target: toolResult.target,
13794
+ sentTextSha256: toolResult.sentTextSha256,
13795
+ goalRevision: Math.max(0, Number(state.meta?.goalContract?.revision || 0)),
13796
+ mutationRevision: Math.max(
13797
+ 0,
13798
+ Number(state.meta?.projectVerification?.mutationRevision || 0)
13799
+ ),
13800
+ };
13801
+ pendingByTarget[toolResult.target] = pending;
13802
+ state.meta.pendingTmuxGitEvidence = Object.fromEntries(
13803
+ Object.entries(pendingByTarget).slice(-8)
13804
+ );
13805
+ toolResult.tmuxGitIntentActions = gitIntent.actions;
13806
+ toolResult.tmuxGitIntentMarkers = gitIntent.markers;
13807
+ return pending;
13808
+ }
13809
+
13810
+ export function attachVerifiedTmuxGitEvidence(state = {}, toolResult = {}) {
13811
+ const pendingByTarget = state.meta?.pendingTmuxGitEvidence || {};
13812
+ const pending = pendingByTarget[toolResult?.target];
13813
+ const verifiedGitActions = inferSuccessfulGitActionsFromTmuxCapture(
13814
+ pending,
13815
+ toolResult
13816
+ );
13817
+ if (!verifiedGitActions.length) return [];
13818
+ toolResult.verifiedGitActions = verifiedGitActions;
13819
+ toolResult.verifiedGitSource = "tmux_capture_pane";
13820
+ toolResult.verifiedGitGoalRevision = Math.max(
13821
+ 0,
13822
+ Number(pending.goalRevision || 0)
13823
+ );
13824
+ toolResult.verifiedGitMutationRevision = Math.max(
13825
+ 0,
13826
+ Number(pending.mutationRevision || 0)
13827
+ );
13828
+ toolResult.verifiedGitCommandSha256 = String(pending.sentTextSha256 || "");
13829
+ delete pendingByTarget[toolResult.target];
13830
+ state.meta.pendingTmuxGitEvidence = pendingByTarget;
13831
+ return verifiedGitActions;
13832
+ }
13833
+
13834
+ export function recordDurableEvidenceCategories(state = {}, toolResult = {}) {
13750
13835
  if (!toolResult || toolResult.ok === false || toolResult.blocked || toolResult.skipped) return;
13751
13836
  state.meta = state.meta || {};
13752
13837
  const categories = new Set(
@@ -13766,33 +13851,50 @@ function recordDurableEvidenceCategories(state = {}, toolResult = {}) {
13766
13851
  categories.add("artifact");
13767
13852
  }
13768
13853
  if (toolName === "read_image") categories.add("visual");
13854
+ const gitActions = toolName === "run_command" && Number(toolResult.exitCode ?? 0) === 0
13855
+ ? inferSuccessfulGitActionsFromCommandResult(toolResult)
13856
+ : Array.isArray(toolResult.verifiedGitActions)
13857
+ ? toolResult.verifiedGitActions
13858
+ : [];
13769
13859
  if (toolName === "run_command" && Number(toolResult.exitCode ?? 0) === 0) {
13770
13860
  categories.add("command");
13771
13861
  const command = String(toolResult.args?.command || "").trim();
13772
- const gitActions = inferSuccessfulGitActionsFromCommandResult(toolResult);
13773
- if (gitActions.length) {
13774
- categories.add("git");
13775
- if (successfulGitCommitProvesFileMutation(toolResult)) categories.add("file");
13776
- state.meta.durableGitActions = [
13777
- ...new Set([
13778
- ...(Array.isArray(state.meta.durableGitActions) ? state.meta.durableGitActions : []),
13779
- ...gitActions,
13780
- ]),
13781
- ];
13782
- const goalRevision = Math.max(0, Number(state.meta?.goalContract?.revision || 0));
13783
- const existingGitEvidence = Array.isArray(state.meta.durableGitEvidence)
13784
- ? state.meta.durableGitEvidence
13785
- : [];
13786
- state.meta.durableGitEvidence = mergeDurableGitEvidence(existingGitEvidence, gitActions, {
13787
- goalRevision,
13788
- mutationRevision: Math.max(
13789
- 0,
13790
- Number(state.meta?.projectVerification?.mutationRevision || 0)
13791
- ),
13792
- });
13793
- }
13794
13862
  if (/\b(?:pytest|unittest|npm\s+test|pnpm\s+test|yarn\s+test)\b/i.test(command)) categories.add("test");
13795
13863
  }
13864
+ if (gitActions.length) {
13865
+ categories.add("git");
13866
+ if (
13867
+ successfulGitCommitProvesFileMutation(toolResult) ||
13868
+ (gitActions.includes("commit") && toolResult.verifiedGitSource === "tmux_capture_pane")
13869
+ ) {
13870
+ categories.add("file");
13871
+ }
13872
+ state.meta.durableGitActions = [
13873
+ ...new Set([
13874
+ ...(Array.isArray(state.meta.durableGitActions) ? state.meta.durableGitActions : []),
13875
+ ...gitActions,
13876
+ ]),
13877
+ ];
13878
+ const goalRevision = Math.max(
13879
+ 0,
13880
+ Number(toolResult.verifiedGitGoalRevision ?? state.meta?.goalContract?.revision ?? 0)
13881
+ );
13882
+ const mutationRevision = Math.max(
13883
+ 0,
13884
+ Number(
13885
+ toolResult.verifiedGitMutationRevision ??
13886
+ state.meta?.projectVerification?.mutationRevision ??
13887
+ 0
13888
+ )
13889
+ );
13890
+ const existingGitEvidence = Array.isArray(state.meta.durableGitEvidence)
13891
+ ? state.meta.durableGitEvidence
13892
+ : [];
13893
+ state.meta.durableGitEvidence = mergeDurableGitEvidence(existingGitEvidence, gitActions, {
13894
+ goalRevision,
13895
+ mutationRevision,
13896
+ });
13897
+ }
13796
13898
  state.meta.durableEvidenceCategories = [...categories];
13797
13899
  }
13798
13900
 
@@ -18727,6 +18829,7 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
18727
18829
  }
18728
18830
  case "tmux_capture_pane": {
18729
18831
  const result = await captureTmuxPane(args, config);
18832
+ attachVerifiedTmuxGitEvidence(state, result);
18730
18833
  const eventResult = sanitizeToolResult(result);
18731
18834
  await store.appendEvent(result.ok ? "tool.completed" : "tool.failed", eventResult);
18732
18835
  observers.event(result.ok ? "tool.completed" : "tool.failed", eventResult);
@@ -18734,6 +18837,7 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
18734
18837
  }
18735
18838
  case "tmux_send_keys": {
18736
18839
  const result = await sendTmuxKeys(args, config);
18840
+ rememberTmuxGitIntent(state, args, result);
18737
18841
  const eventResult = sanitizeToolResult(result);
18738
18842
  await store.appendEvent(result.ok ? "tool.completed" : "tool.failed", eventResult);
18739
18843
  observers.event(result.ok ? "tool.completed" : "tool.failed", eventResult);
@@ -196,6 +196,63 @@ export function dockerWorkspaceAliasMounts(
196
196
  return workspace === DOCKER_WORKSPACE ? [] : [workspace];
197
197
  }
198
198
 
199
+ function pathIsWithin(parentPath, candidatePath) {
200
+ const relative = path.relative(parentPath, candidatePath);
201
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
202
+ }
203
+
204
+ function linkedWorktreeCommonGitDir(workspace) {
205
+ const dotGitPath = path.join(workspace, ".git");
206
+ let dotGit;
207
+ try {
208
+ if (!fsSync.statSync(dotGitPath).isFile()) return "";
209
+ dotGit = fsSync.readFileSync(dotGitPath, "utf8");
210
+ } catch {
211
+ return "";
212
+ }
213
+ const match = String(dotGit).match(/^gitdir:\s*(.+?)\s*$/im);
214
+ if (!match?.[1]) return "";
215
+ const gitDir = path.resolve(workspace, match[1]);
216
+ let commonDir = gitDir;
217
+ try {
218
+ const commonPath = path.join(gitDir, "commondir");
219
+ if (fsSync.existsSync(commonPath)) {
220
+ commonDir = path.resolve(gitDir, fsSync.readFileSync(commonPath, "utf8").trim());
221
+ }
222
+ const resolvedGitDir = fsSync.realpathSync(gitDir);
223
+ const resolvedCommonDir = fsSync.realpathSync(commonDir);
224
+ if (
225
+ !pathIsWithin(resolvedCommonDir, resolvedGitDir) ||
226
+ !fsSync.existsSync(path.join(resolvedCommonDir, "HEAD")) ||
227
+ !fsSync.existsSync(path.join(resolvedCommonDir, "objects"))
228
+ ) {
229
+ return "";
230
+ }
231
+ return resolvedCommonDir;
232
+ } catch {
233
+ return "";
234
+ }
235
+ }
236
+
237
+ export function dockerWritableGitMounts(
238
+ command = "",
239
+ config = {},
240
+ policy = {},
241
+ sandboxMode = normalizeSandboxMode(config.sandboxMode)
242
+ ) {
243
+ if (
244
+ sandboxMode !== "docker-workspace" ||
245
+ policy.gitOnly !== true ||
246
+ !/\bgit\s+(?:add|commit|rm|mv)\b/i.test(String(command || ""))
247
+ ) {
248
+ return [];
249
+ }
250
+ const workspace = path.resolve(String(config.commandCwd || process.cwd()));
251
+ const commonDir = linkedWorktreeCommonGitDir(workspace);
252
+ if (!commonDir || pathIsWithin(workspace, commonDir)) return [];
253
+ return uniqueHostMounts([commonDir]);
254
+ }
255
+
199
256
  async function ensurePersistentDockerDirs(config) {
200
257
  const dirs = persistentDockerDirs(config);
201
258
  await Promise.all([fs.mkdir(dirs.home, { recursive: true }), fs.mkdir(dirs.cache, { recursive: true }), fs.mkdir(dirs.env, { recursive: true })]);
@@ -381,6 +438,12 @@ function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command,
381
438
  "-v",
382
439
  `${mount}:${mount}:${mountMode}`,
383
440
  ]);
441
+ const writableGitMountArgs = dockerWritableGitMounts(
442
+ command,
443
+ config,
444
+ policy,
445
+ sandboxMode
446
+ ).flatMap((mount) => ["-v", `${mount}:${mount}:rw`]);
384
447
  const readOnlyArgs =
385
448
  mountMode === "ro"
386
449
  ? ["--read-only", "--tmpfs", "/tmp:rw,nosuid,nodev,size=128m"]
@@ -412,6 +475,7 @@ function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command,
412
475
  ? ["-e", `GIT_CONFIG_GLOBAL=${DOCKER_GIT_CONFIG}`]
413
476
  : []),
414
477
  ...readOnlyHostMountArgs,
478
+ ...writableGitMountArgs,
415
479
  ...workspaceAliasMountArgs,
416
480
  "-v",
417
481
  `${config.commandCwd}:${DOCKER_WORKSPACE}:${mountMode}`,
@@ -302,7 +302,10 @@ async function persistMarkdownArtifact(store, config, subdir, stem, payload) {
302
302
  }
303
303
 
304
304
  if (config?.commandCwd) {
305
- const workspaceDir = path.join(config.commandCwd, "artifacts", subdir);
305
+ // Perception reports are runtime evidence, not task-owned source. Keep the
306
+ // workspace-relative copy under the standard ignored AgInTi directory so
307
+ // visual validation cannot dirty an otherwise completed repository.
308
+ const workspaceDir = path.join(config.commandCwd, ".aginti", "artifacts", subdir);
306
309
  await fs.mkdir(workspaceDir, { recursive: true });
307
310
  const workspacePath = path.join(workspaceDir, filename);
308
311
  await fs.writeFile(workspacePath, content, "utf8");
@@ -2304,7 +2304,8 @@ export function selectProgressiveTools(
2304
2304
  if (
2305
2305
  config.taskOwnedCommitPending === true &&
2306
2306
  config.testFailureRepairActive !== true &&
2307
- config.testVerificationPending !== true
2307
+ config.testVerificationPending !== true &&
2308
+ config.requiredProjectCommandPending !== true
2308
2309
  ) {
2309
2310
  const available = new Map(enabled.map(({ name, tool }) => [name, tool]));
2310
2311
  const pendingGitActions = Array.isArray(config.taskOwnedPendingGitActions)
@@ -2332,6 +2333,18 @@ export function selectProgressiveTools(
2332
2333
  config.testVerificationPending !== true
2333
2334
  ) {
2334
2335
  const available = new Map(enabled.map(({ name, tool }) => [name, tool]));
2336
+ if (
2337
+ config.requiredProjectCommandPending === true &&
2338
+ typeof config.requiredProjectCommand === "string" &&
2339
+ config.requiredProjectCommand.trim()
2340
+ ) {
2341
+ const requiredCommand = constrainRunCommand(
2342
+ available.get("run_command"),
2343
+ config.requiredProjectCommand,
2344
+ "Run this exact outstanding project command before Git completion or final artifact acceptance. Preserve the generated outputs and do not substitute another status, commit, or discovery command."
2345
+ );
2346
+ return [requiredCommand, finish].filter(Boolean);
2347
+ }
2335
2348
  const pendingGitActions = Array.isArray(config.artifactValidationPendingGitActions)
2336
2349
  ? config.artifactValidationPendingGitActions.map((item) => String(item || "").toLowerCase())
2337
2350
  : [];
@@ -2120,6 +2120,147 @@ export function inferSuccessfulGitActionsFromCommandResult(payload = {}) {
2120
2120
  return [];
2121
2121
  }
2122
2122
 
2123
+ export function buildTmuxGitIntent(command = "") {
2124
+ const text = String(command || "");
2125
+ // `$?` is expected in the explicit tmux exit marker. Remove only that
2126
+ // non-command expansion before parsing; all other shell expansion remains
2127
+ // subject to the normal ambiguity guard.
2128
+ const parseable = text.replace(/\$\?/g, "0");
2129
+ const actions = inferGitActionsFromCommand(parseable, {
2130
+ requireFailurePropagation: false,
2131
+ }).filter((action) => !isObservationalGitAction(action));
2132
+ const markers = [
2133
+ ...text.matchAll(/={3,}([A-Z0-9][A-Z0-9_-]{3,96}(?:START|BEGIN))={3,}/g),
2134
+ ].map((match) => String(match[0] || ""));
2135
+ if (!actions.length || !markers.length) return null;
2136
+ return {
2137
+ actions: unique(actions),
2138
+ markers: unique(markers).slice(0, 4),
2139
+ };
2140
+ }
2141
+
2142
+ export function inferSuccessfulGitActionsFromTmuxCapture(intent = {}, payload = {}) {
2143
+ if (
2144
+ !intent ||
2145
+ !payload ||
2146
+ payload.ok === false ||
2147
+ payload.blocked ||
2148
+ String(payload.toolName || "") !== "tmux_capture_pane"
2149
+ ) {
2150
+ return [];
2151
+ }
2152
+ const content = [payload.content, payload.contentPreview, payload.contentTail]
2153
+ .map((item) => String(item || ""))
2154
+ .filter(Boolean)
2155
+ .join("\n");
2156
+ const markers = Array.isArray(intent.markers)
2157
+ ? intent.markers.map(String).filter(Boolean)
2158
+ : [];
2159
+ if (!content || !markers.length || !markers.some((marker) => content.includes(marker))) {
2160
+ return [];
2161
+ }
2162
+ const requested = Array.isArray(intent.actions)
2163
+ ? intent.actions.map((item) => String(item || "").toLowerCase()).filter(Boolean)
2164
+ : [];
2165
+ const verified = [];
2166
+ if (
2167
+ requested.includes("commit") &&
2168
+ /={3,}(?:GIT_)?COMMIT_EXIT\s*[:=]\s*0={3,}/i.test(content) &&
2169
+ /^\[[^\]\n]+\s+[0-9a-f]{7,}\]\s+\S+/mi.test(content)
2170
+ ) {
2171
+ verified.push("commit");
2172
+ }
2173
+ return verified;
2174
+ }
2175
+
2176
+ export function reconcileTmuxGitEvidenceEvents(events = []) {
2177
+ const source = Array.isArray(events) ? events : [];
2178
+ const pendingStarts = new Map();
2179
+ const activeIntents = new Map();
2180
+ let goalRevision = 0;
2181
+ let mutationRevision = 0;
2182
+
2183
+ return source.map((event) => {
2184
+ const type = String(event?.type || "");
2185
+ const data = event?.data && typeof event.data === "object" ? event.data : {};
2186
+ goalRevision = Math.max(
2187
+ goalRevision,
2188
+ Number(
2189
+ data.goalRevision ??
2190
+ (type === "goal.updated" ? data.revision : 0) ??
2191
+ 0
2192
+ ) || 0
2193
+ );
2194
+ mutationRevision = Math.max(
2195
+ mutationRevision,
2196
+ Number(data.projectMutationRevision ?? data.mutationRevision ?? 0) || 0
2197
+ );
2198
+
2199
+ const toolName = String(data.toolName || "");
2200
+ const target = String(data.target || data.args?.target || "");
2201
+ if (type === "tool.started" && toolName === "tmux_send_keys" && target) {
2202
+ const intent = buildTmuxGitIntent(data.args?.text || "");
2203
+ if (intent) {
2204
+ pendingStarts.set(target, {
2205
+ ...intent,
2206
+ target,
2207
+ goalRevision,
2208
+ mutationRevision,
2209
+ });
2210
+ } else {
2211
+ pendingStarts.delete(target);
2212
+ activeIntents.delete(target);
2213
+ }
2214
+ return event;
2215
+ }
2216
+
2217
+ if (["tool.completed", "tool.failed"].includes(type) && toolName === "tmux_send_keys" && target) {
2218
+ const pending = pendingStarts.get(target);
2219
+ pendingStarts.delete(target);
2220
+ if (type === "tool.completed" && data.ok !== false && pending) {
2221
+ activeIntents.set(target, {
2222
+ ...pending,
2223
+ sentTextSha256: String(data.sentTextSha256 || ""),
2224
+ });
2225
+ } else {
2226
+ activeIntents.delete(target);
2227
+ }
2228
+ return event;
2229
+ }
2230
+
2231
+ if (type === "tool.completed" && toolName === "tmux_capture_pane" && target) {
2232
+ const pending = activeIntents.get(target);
2233
+ const verifiedGitActions = inferSuccessfulGitActionsFromTmuxCapture(
2234
+ pending,
2235
+ data
2236
+ );
2237
+ if (verifiedGitActions.length) {
2238
+ activeIntents.delete(target);
2239
+ return {
2240
+ ...event,
2241
+ data: {
2242
+ ...data,
2243
+ verifiedGitActions,
2244
+ verifiedGitSource: "tmux_capture_pane",
2245
+ verifiedGitGoalRevision: Math.max(
2246
+ 0,
2247
+ Number(pending.goalRevision || 0)
2248
+ ),
2249
+ verifiedGitMutationRevision: Math.max(
2250
+ 0,
2251
+ Number(pending.mutationRevision || 0)
2252
+ ),
2253
+ verifiedGitCommandSha256: String(
2254
+ pending.sentTextSha256 || ""
2255
+ ),
2256
+ },
2257
+ };
2258
+ }
2259
+ }
2260
+ return event;
2261
+ });
2262
+ }
2263
+
2123
2264
  export function successfulGitCommitProvesFileMutation(payload = {}) {
2124
2265
  if (!inferSuccessfulGitActionsFromCommandResult(payload).includes("commit")) return false;
2125
2266
  const output = String(payload.stdout || payload.result || "");
@@ -3955,7 +4096,9 @@ function toolPayloadToEvidence(payload = {}, source = "tool") {
3955
4096
  if (["read_image", "generate_image"].includes(toolName) || /\b(screenshot|visible|thumbnail|preview|image)\b/.test(text)) {
3956
4097
  push("visual", `${toolName || "tool"} supplied visual evidence`, payload.path || payload.outputPath || args.path || "");
3957
4098
  }
3958
- const gitActions = inferSuccessfulGitActionsFromCommandResult(payload);
4099
+ const gitActions = Array.isArray(payload.verifiedGitActions)
4100
+ ? payload.verifiedGitActions
4101
+ : inferSuccessfulGitActionsFromCommandResult(payload);
3959
4102
  if (gitActions.length) {
3960
4103
  push(
3961
4104
  "git",
@@ -3964,8 +4107,18 @@ function toolPayloadToEvidence(payload = {}, source = "tool") {
3964
4107
  {
3965
4108
  gitAction: gitActions.at(-1),
3966
4109
  gitActions,
3967
- goalRevision: Math.max(0, Number(payload.goalRevision || 0)),
3968
- mutationRevision: Math.max(0, Number(payload.projectMutationRevision || 0)),
4110
+ goalRevision: Math.max(
4111
+ 0,
4112
+ Number(payload.verifiedGitGoalRevision ?? payload.goalRevision ?? 0)
4113
+ ),
4114
+ mutationRevision: Math.max(
4115
+ 0,
4116
+ Number(
4117
+ payload.verifiedGitMutationRevision ??
4118
+ payload.projectMutationRevision ??
4119
+ 0
4120
+ )
4121
+ ),
3969
4122
  }
3970
4123
  );
3971
4124
  }
@@ -4038,7 +4191,9 @@ function messageToEvidence(message = {}) {
4038
4191
  }
4039
4192
 
4040
4193
  export function buildScsEvidenceLedger({ state = {}, context = {} } = {}) {
4041
- const events = Array.isArray(context.events) ? context.events : [];
4194
+ const events = reconcileTmuxGitEvidenceEvents(
4195
+ Array.isArray(context.events) ? context.events : []
4196
+ );
4042
4197
  const messages = Array.isArray(state.messages) ? state.messages : [];
4043
4198
  const eventEvidence = events.flatMap(eventToEvidence);
4044
4199
  // Completed tools are mirrored into both the append-only event stream and