@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.
- package/docs/perception-and-web-research.md +2 -1
- package/docs/runtime-modes-and-autonomy.md +4 -0
- package/docs/supervision-campaign-ledger.md +39 -0
- package/package.json +1 -1
- package/scripts/smoke-docker-command.js +42 -0
- package/scripts/smoke-dynamic-step-budget.js +92 -0
- package/scripts/smoke-model-roles.js +198 -0
- package/scripts/smoke-perception-research.js +4 -0
- package/scripts/smoke-progressive-tool-selection.js +24 -0
- package/src/agent-runner.js +130 -26
- package/src/docker-sandbox.js +64 -0
- package/src/perception-tools.js +4 -1
- package/src/progressive-tool-selection.js +14 -1
- package/src/scs-evidence.js +159 -4
|
@@ -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
|
|
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.
|
|
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.",
|
package/src/agent-runner.js
CHANGED
|
@@ -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
|
-
|
|
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, {
|
|
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
|
|
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);
|
package/src/docker-sandbox.js
CHANGED
|
@@ -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}`,
|
package/src/perception-tools.js
CHANGED
|
@@ -302,7 +302,10 @@ async function persistMarkdownArtifact(store, config, subdir, stem, payload) {
|
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
if (config?.commandCwd) {
|
|
305
|
-
|
|
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
|
: [];
|
package/src/scs-evidence.js
CHANGED
|
@@ -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 =
|
|
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(
|
|
3968
|
-
|
|
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 =
|
|
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
|