@lazyingart/agintiflow 0.20.212 → 0.20.214
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/supervision-campaign-ledger.md +62 -0
- package/package.json +1 -1
- package/scripts/smoke-dynamic-step-budget.js +192 -0
- package/scripts/smoke-model-roles.js +23 -12
- package/scripts/smoke-scs-evidence-visibility.js +43 -31
- package/scripts/smoke-truthful-completion.js +57 -4
- package/scripts/smoke-web-api.js +19 -4
- package/src/agent-runner.js +115 -3
- package/src/model-client.js +5 -1
- package/src/scs-evidence.js +130 -25
- package/web.js +23 -7
|
@@ -96,3 +96,65 @@ while retaining the artifact gate for real generated figures. Exact session
|
|
|
96
96
|
evidence remains in
|
|
97
97
|
`~/.agintiflow/sessions/aginti-qa-incident-metrics-001/events.jsonl` and the
|
|
98
98
|
machine ledger records the run as `passed_after_fix`.
|
|
99
|
+
|
|
100
|
+
### GitHub maintenance hidden acceptance and goal-scoped evidence
|
|
101
|
+
|
|
102
|
+
`github-safe-maintenance-012` started from a normal maintenance prompt rather
|
|
103
|
+
than a checker-shaped instruction. The agent repaired and verified the target,
|
|
104
|
+
but its first completion omitted the exact `docs/maintenance-handoff.md` file
|
|
105
|
+
required by independent acceptance. The same retained session inspected the
|
|
106
|
+
external failure, created the missing handoff, committed and pushed target
|
|
107
|
+
commit `36fa6c0`, and left `main` clean and synchronized. The hidden
|
|
108
|
+
`github_maintenance_contract.py` checker then passed.
|
|
109
|
+
|
|
110
|
+
Supervision of the repair exposed four runtime defects that could affect other
|
|
111
|
+
profiles:
|
|
112
|
+
|
|
113
|
+
- A test command wrapped as `command; echo "EXIT:$?"` could have shell exit zero
|
|
114
|
+
even when the real command failed. Explicit final `EXIT`, `STATUS`, or
|
|
115
|
+
`RESULT` probes are now parsed; a missing or nonzero marker is failing
|
|
116
|
+
evidence.
|
|
117
|
+
- A genuinely new continuation could inherit completed artifact, SCS,
|
|
118
|
+
project-verification, and repair state from the prior goal. New goals now
|
|
119
|
+
clear only goal-scoped execution evidence while preserving conversation,
|
|
120
|
+
durable goal history, and goal-keyed research memory.
|
|
121
|
+
- An acceptance sentence listing screenshots, PDFs, reports, or app launches
|
|
122
|
+
"as appropriate" could force irrelevant visual work. Optional evidence
|
|
123
|
+
examples no longer become mandatory categories.
|
|
124
|
+
- Merely naming a read-only checker such as `contract.py` could force a file
|
|
125
|
+
artifact. File evidence now requires both mutation intent and a workspace
|
|
126
|
+
file/source target; a virtual canvas artifact remains artifact evidence only.
|
|
127
|
+
|
|
128
|
+
The patched source resumed `aginti-github-maintenance-001`, invoked the hidden
|
|
129
|
+
checker exactly once, performed no file, canvas, commit, or push side effect,
|
|
130
|
+
and completed with a clean repository-state check. The full npm suite and the
|
|
131
|
+
focused dynamic-budget, SCS/model-role, and web-canvas regressions pass for
|
|
132
|
+
AgInTiFlow `0.20.213`.
|
|
133
|
+
|
|
134
|
+
### Java repair, permission pause, and durable artifact evidence
|
|
135
|
+
|
|
136
|
+
`java-event-window-013` passed after exposing two runtime defects with a normal,
|
|
137
|
+
underspecified Java repair prompt. DeepSeek correctly implemented decimal
|
|
138
|
+
duration parsing, non-mutating percentile interpolation, deterministic window
|
|
139
|
+
summaries, project guidance, and generated-output ignores. The first run then
|
|
140
|
+
encountered a host permission blocker while invoking the checked-in test script.
|
|
141
|
+
The runtime continued spending model and SCS turns instead of persisting a
|
|
142
|
+
single actionable pause. After trusted-host approval, loose artifact inference
|
|
143
|
+
also treated a generated test transcript as a mandatory deliverable, prompting
|
|
144
|
+
the agent to create and commit an unrequested `docs/test-results.txt`.
|
|
145
|
+
|
|
146
|
+
Permission advice that cannot auto-recover now stops the run immediately with
|
|
147
|
+
durable resume data. Approval is single-use, and a resolved blocker cannot be
|
|
148
|
+
replayed by a stale web request. Same-task continuation still preserves the
|
|
149
|
+
original goal when an approval sentence precedes the continuation instruction.
|
|
150
|
+
Artifact evidence from commands is accepted only when it names a supported,
|
|
151
|
+
existing, nonempty path; removed files and label-only prose no longer satisfy
|
|
152
|
+
completion. Exclusion language such as "ignore generated build and session
|
|
153
|
+
outputs" no longer invents an artifact requirement.
|
|
154
|
+
|
|
155
|
+
The queued correction was applied to the same live session, the stray transcript
|
|
156
|
+
commit was removed, and the intended repair remains at target commit `f2792f3`.
|
|
157
|
+
Independent verification passed the checked-in Java test script, the hidden
|
|
158
|
+
event-window contract, generated-output tracking checks, clean-worktree checks,
|
|
159
|
+
the focused permission/evidence regressions, and the complete AgInTiFlow npm
|
|
160
|
+
suite.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.214",
|
|
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",
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
recordProjectVerificationOutcome,
|
|
27
27
|
recordExactOutputProgress,
|
|
28
28
|
recordStaticDiscoveryProgress,
|
|
29
|
+
resetGoalScopedRuntimeState,
|
|
29
30
|
resetStaticDiscoveryAfterContextLoss,
|
|
30
31
|
rememberCompletedDeepResearch,
|
|
31
32
|
repeatedNoProgressToolBlock,
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
import {
|
|
38
39
|
augmentScsTaskContractWithProjectVerification,
|
|
39
40
|
buildScsEvidenceLedger,
|
|
41
|
+
deriveScsTaskContract,
|
|
40
42
|
evaluateScsEvidence,
|
|
41
43
|
} from "../src/scs-evidence.js";
|
|
42
44
|
import {
|
|
@@ -72,6 +74,196 @@ function toolMessage(payload) {
|
|
|
72
74
|
|
|
73
75
|
try {
|
|
74
76
|
assert(normalizeDynamicStepsMode("off") === "off", "dynamic mode off did not normalize");
|
|
77
|
+
const failedExitProbeState = { meta: {} };
|
|
78
|
+
const failedExitProbeResult = {
|
|
79
|
+
toolName: "run_command",
|
|
80
|
+
ok: true,
|
|
81
|
+
exitCode: 0,
|
|
82
|
+
args: { command: 'python -m unittest discover -s tests; echo "EXIT:$?"' },
|
|
83
|
+
stdout: "external contract did not pass\nEXIT:1\n",
|
|
84
|
+
stderr: "",
|
|
85
|
+
};
|
|
86
|
+
recordProjectVerificationOutcome(failedExitProbeState, failedExitProbeResult, {
|
|
87
|
+
commandCwd: workspace,
|
|
88
|
+
taskProfile: "qa",
|
|
89
|
+
});
|
|
90
|
+
assert(
|
|
91
|
+
failedExitProbeResult.projectTest?.passed === false &&
|
|
92
|
+
failedExitProbeResult.projectTest?.explicitExitStatus === 1,
|
|
93
|
+
"a wrapped nonzero test status was recorded as passing"
|
|
94
|
+
);
|
|
95
|
+
const missingExitProbeState = { meta: {} };
|
|
96
|
+
const missingExitProbeResult = {
|
|
97
|
+
toolName: "run_command",
|
|
98
|
+
ok: true,
|
|
99
|
+
exitCode: 0,
|
|
100
|
+
args: { command: 'python -m unittest discover -s tests; printf "EXIT=%s\\n" "$?"' },
|
|
101
|
+
stdout: "external contract output ended before the status marker\n",
|
|
102
|
+
stderr: "",
|
|
103
|
+
};
|
|
104
|
+
recordProjectVerificationOutcome(missingExitProbeState, missingExitProbeResult, {
|
|
105
|
+
commandCwd: workspace,
|
|
106
|
+
taskProfile: "qa",
|
|
107
|
+
});
|
|
108
|
+
assert(
|
|
109
|
+
missingExitProbeResult.projectTest?.passed === false &&
|
|
110
|
+
missingExitProbeResult.projectTest?.explicitExitStatus === null,
|
|
111
|
+
"a missing wrapped test status was recorded as passing"
|
|
112
|
+
);
|
|
113
|
+
const passingExitProbeState = { meta: {} };
|
|
114
|
+
const passingExitProbeResult = {
|
|
115
|
+
toolName: "run_command",
|
|
116
|
+
ok: true,
|
|
117
|
+
exitCode: 0,
|
|
118
|
+
args: { command: 'python -m unittest discover -s tests; echo "EXIT=$?"' },
|
|
119
|
+
stdout: "contract checks passed\nEXIT=0\n",
|
|
120
|
+
stderr: "",
|
|
121
|
+
};
|
|
122
|
+
recordProjectVerificationOutcome(passingExitProbeState, passingExitProbeResult, {
|
|
123
|
+
commandCwd: workspace,
|
|
124
|
+
taskProfile: "qa",
|
|
125
|
+
});
|
|
126
|
+
assert(
|
|
127
|
+
passingExitProbeResult.projectTest?.passed === true &&
|
|
128
|
+
passingExitProbeResult.projectTest?.explicitExitStatus === 0,
|
|
129
|
+
"a wrapped zero test status was not accepted"
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const newGoalState = {
|
|
133
|
+
meta: {
|
|
134
|
+
artifactProgress: { complete: true },
|
|
135
|
+
completionEvidenceRepair: { attempts: 1 },
|
|
136
|
+
dataProjectWorkflow: { ready: true },
|
|
137
|
+
durableEvidenceCategories: ["file", "visual"],
|
|
138
|
+
durableGitActions: ["commit"],
|
|
139
|
+
durableGitEvidence: [{ action: "commit", goalRevision: 1 }],
|
|
140
|
+
failedTestRecoveryPacket: { content: "old failure" },
|
|
141
|
+
goalContract: { revision: 2 },
|
|
142
|
+
projectVerification: { mutationRevision: 4 },
|
|
143
|
+
scs: { taskContract: { exactOutputPaths: ["old-output.md"] } },
|
|
144
|
+
completedDeepResearch: [{ goalKey: "retained-other-goal" }],
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
const removedGoalState = resetGoalScopedRuntimeState(newGoalState);
|
|
148
|
+
assert(removedGoalState.includes("artifactProgress"), "new goal did not clear stale artifact progress");
|
|
149
|
+
assert(!newGoalState.meta.projectVerification, "new goal retained stale project verification");
|
|
150
|
+
assert(!newGoalState.meta.scs, "new goal retained the previous SCS task contract");
|
|
151
|
+
assert(!newGoalState.meta.durableEvidenceCategories, "new goal inherited completed evidence categories");
|
|
152
|
+
assert(newGoalState.meta.goalContract?.revision === 2, "new goal reset its durable goal contract");
|
|
153
|
+
assert(newGoalState.meta.completedDeepResearch?.length === 1, "new goal discarded goal-keyed research cache");
|
|
154
|
+
|
|
155
|
+
const optionalVisualContract = deriveScsTaskContract({
|
|
156
|
+
goal: "Repair the repository, verify it, commit, and push the intentional work.",
|
|
157
|
+
taskProfile: "github",
|
|
158
|
+
acceptanceCriteria: [
|
|
159
|
+
"Do not rely only on chat summaries; verify files, commands, screenshots, PDFs, reports, or app launches as appropriate.",
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
assert(
|
|
163
|
+
!optionalVisualContract.requiredEvidence.some((item) => item.category === "visual"),
|
|
164
|
+
"an optional evidence example forced irrelevant visual validation"
|
|
165
|
+
);
|
|
166
|
+
const explicitVisualContract = deriveScsTaskContract({
|
|
167
|
+
goal: "Capture and inspect a screenshot of the repaired interface.",
|
|
168
|
+
taskProfile: "website",
|
|
169
|
+
});
|
|
170
|
+
assert(
|
|
171
|
+
explicitVisualContract.requiredEvidence.some((item) => item.category === "visual"),
|
|
172
|
+
"an explicit screenshot request lost visual validation"
|
|
173
|
+
);
|
|
174
|
+
const readOnlyCheckerContract = deriveScsTaskContract({
|
|
175
|
+
goal:
|
|
176
|
+
"Re-run /tmp/acceptance/github_maintenance_contract.py once, verify the repository, and do not edit, commit, or push anything.",
|
|
177
|
+
taskProfile: "github",
|
|
178
|
+
});
|
|
179
|
+
assert(
|
|
180
|
+
!readOnlyCheckerContract.requiredEvidence.some((item) => item.category === "file"),
|
|
181
|
+
"a read-only checker path was mistaken for a requested file change"
|
|
182
|
+
);
|
|
183
|
+
const canvasOnlyContract = deriveScsTaskContract({
|
|
184
|
+
goal: "Create a canvas artifact preview for this smoke test.",
|
|
185
|
+
});
|
|
186
|
+
assert(
|
|
187
|
+
canvasOnlyContract.requiredEvidence.some((item) => item.category === "artifact") &&
|
|
188
|
+
!canvasOnlyContract.requiredEvidence.some((item) => item.category === "file"),
|
|
189
|
+
"a virtual canvas artifact was mistaken for a workspace-file mutation"
|
|
190
|
+
);
|
|
191
|
+
const sourceRepairContract = deriveScsTaskContract({
|
|
192
|
+
goal: "Fix src/runtime.py and verify the focused tests.",
|
|
193
|
+
taskProfile: "python",
|
|
194
|
+
});
|
|
195
|
+
assert(
|
|
196
|
+
sourceRepairContract.requiredEvidence.some((item) => item.category === "file"),
|
|
197
|
+
"an explicit source repair lost its file-change evidence gate"
|
|
198
|
+
);
|
|
199
|
+
const ignoredGeneratedOutputsContract = deriveScsTaskContract({
|
|
200
|
+
goal: [
|
|
201
|
+
"You have explicit trusted-host approval for this isolated Java fixture.",
|
|
202
|
+
"Continue the same task from the current edits. Run the checked-in project test script, repair any failures,",
|
|
203
|
+
"create the required project guidance, ignore generated build and session outputs, commit only intentional work,",
|
|
204
|
+
"and finish with verified evidence.",
|
|
205
|
+
].join(" "),
|
|
206
|
+
taskProfile: "java",
|
|
207
|
+
});
|
|
208
|
+
assert(
|
|
209
|
+
ignoredGeneratedOutputsContract.requiredEvidence.some((item) => item.category === "file") &&
|
|
210
|
+
ignoredGeneratedOutputsContract.requiredEvidence.some((item) => item.category === "command") &&
|
|
211
|
+
ignoredGeneratedOutputsContract.requiredEvidence.some((item) => item.category === "git") &&
|
|
212
|
+
!ignoredGeneratedOutputsContract.requiredEvidence.some((item) => item.category === "artifact"),
|
|
213
|
+
"ignoring generated build/session outputs invented a standalone artifact requirement"
|
|
214
|
+
);
|
|
215
|
+
const durableArtifactPath = path.join(workspace, "reports", "durable-report.pdf");
|
|
216
|
+
await fs.mkdir(path.dirname(durableArtifactPath), { recursive: true });
|
|
217
|
+
await fs.writeFile(durableArtifactPath, "%PDF-1.4\nsmoke\n", "utf8");
|
|
218
|
+
const artifactEvents = [
|
|
219
|
+
{
|
|
220
|
+
type: "tool.completed",
|
|
221
|
+
data: {
|
|
222
|
+
ok: true,
|
|
223
|
+
toolName: "run_command",
|
|
224
|
+
args: { command: "printf smoke > reports/durable-report.pdf" },
|
|
225
|
+
stdout: "created reports/durable-report.pdf",
|
|
226
|
+
exitCode: 0,
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
const durableArtifactLedger = buildScsEvidenceLedger({
|
|
231
|
+
context: { events: artifactEvents, commandCwd: workspace },
|
|
232
|
+
});
|
|
233
|
+
assert(
|
|
234
|
+
durableArtifactLedger.categories.includes("artifact"),
|
|
235
|
+
"an existing shell-generated PDF did not count as durable artifact evidence"
|
|
236
|
+
);
|
|
237
|
+
await fs.rm(durableArtifactPath);
|
|
238
|
+
const removedArtifactLedger = buildScsEvidenceLedger({
|
|
239
|
+
context: { events: artifactEvents, commandCwd: workspace },
|
|
240
|
+
});
|
|
241
|
+
assert(
|
|
242
|
+
!removedArtifactLedger.categories.includes("artifact") &&
|
|
243
|
+
removedArtifactLedger.items.some((item) => item.category === "artifact" && item.verified === false),
|
|
244
|
+
"a removed artifact continued to satisfy the final evidence ledger"
|
|
245
|
+
);
|
|
246
|
+
const labelOnlyArtifactLedger = buildScsEvidenceLedger({
|
|
247
|
+
context: {
|
|
248
|
+
events: [
|
|
249
|
+
{
|
|
250
|
+
type: "tool.completed",
|
|
251
|
+
data: {
|
|
252
|
+
ok: true,
|
|
253
|
+
toolName: "run_command",
|
|
254
|
+
args: { command: "echo ARTIFACT READY" },
|
|
255
|
+
stdout: "ARTIFACT READY",
|
|
256
|
+
exitCode: 0,
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
commandCwd: workspace,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
assert(
|
|
264
|
+
!labelOnlyArtifactLedger.categories.includes("artifact"),
|
|
265
|
+
"an artifact label in generic shell text counted as a durable artifact"
|
|
266
|
+
);
|
|
75
267
|
assert(
|
|
76
268
|
completionEvidenceNeedsCommand({ missingProjectCommands: ["python analysis.py"] }),
|
|
77
269
|
"a pending canonical command did not reopen command execution"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import { fileURLToPath } from "node:url";
|
|
5
7
|
import {
|
|
@@ -1086,18 +1088,27 @@ assert(
|
|
|
1086
1088
|
!missingWriterToolEval.ok && missingWriterToolEval.missingToolCalls.includes("writing_specialist"),
|
|
1087
1089
|
"SCS should reject finish when required specialist call is missing"
|
|
1088
1090
|
);
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1091
|
+
const writerEvidenceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "aginti-writer-evidence-"));
|
|
1092
|
+
try {
|
|
1093
|
+
const writerArtifactPath = path.join(writerEvidenceRoot, "artifacts", "writer.json");
|
|
1094
|
+
fs.mkdirSync(path.dirname(writerArtifactPath), { recursive: true });
|
|
1095
|
+
fs.writeFileSync(writerArtifactPath, JSON.stringify({ status: "completed" }), "utf8");
|
|
1096
|
+
const presentWriterToolEval = evaluateScsEvidence(
|
|
1097
|
+
requiredWriterToolContract,
|
|
1098
|
+
buildScsEvidenceLedger({
|
|
1099
|
+
context: {
|
|
1100
|
+
commandCwd: writerEvidenceRoot,
|
|
1101
|
+
events: [
|
|
1102
|
+
{ type: "file.changed", data: { path: "final/story.md" } },
|
|
1103
|
+
{ type: "tool.completed", data: { toolName: "writing_specialist", ok: true, artifactPath: writerArtifactPath } },
|
|
1104
|
+
],
|
|
1105
|
+
},
|
|
1106
|
+
})
|
|
1107
|
+
);
|
|
1108
|
+
assert(presentWriterToolEval.ok, "SCS should accept required specialist call when tool evidence is present");
|
|
1109
|
+
} finally {
|
|
1110
|
+
fs.rmSync(writerEvidenceRoot, { recursive: true, force: true });
|
|
1111
|
+
}
|
|
1101
1112
|
|
|
1102
1113
|
const blockedFileFinish = await reviewScsFinish(
|
|
1103
1114
|
{ mock: true },
|
|
@@ -152,38 +152,50 @@ const finishDecision = await reviewScsFinish(
|
|
|
152
152
|
assert.equal(finishDecision.decision, "finish_allowed", "SCS should not reject finish as no-evidence when the contract ledger is satisfied");
|
|
153
153
|
assert.match(finishDecision.reason, /Overrode a no-evidence finish rejection/);
|
|
154
154
|
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
155
|
+
const researchEvidenceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "aginti-research-evidence-"));
|
|
156
|
+
try {
|
|
157
|
+
const reportPath = path.join(researchEvidenceRoot, "reports", "cited-research.md");
|
|
158
|
+
const artifactPath = path.join(researchEvidenceRoot, "artifacts", "deep-research.json");
|
|
159
|
+
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
160
|
+
fs.mkdirSync(path.dirname(artifactPath), { recursive: true });
|
|
161
|
+
fs.writeFileSync(reportPath, "# Cited research\n\nVerified report fixture.\n", "utf8");
|
|
162
|
+
fs.writeFileSync(artifactPath, JSON.stringify({ status: "completed" }), "utf8");
|
|
163
|
+
|
|
164
|
+
const researchReportLedger = buildScsEvidenceLedger({
|
|
165
|
+
context: {
|
|
166
|
+
events: [{
|
|
167
|
+
type: "tool.completed",
|
|
168
|
+
data: {
|
|
169
|
+
ok: true,
|
|
170
|
+
toolName: "deep_research",
|
|
171
|
+
status: "completed",
|
|
172
|
+
reportPath,
|
|
173
|
+
artifactPath,
|
|
174
|
+
coverage: {
|
|
175
|
+
verifiedClaimCount: 12,
|
|
176
|
+
quoteVerificationRate: 1,
|
|
177
|
+
},
|
|
178
|
+
audit: {
|
|
179
|
+
citationCoverage: 1,
|
|
180
|
+
unknownEvidenceIds: [],
|
|
181
|
+
},
|
|
172
182
|
},
|
|
173
|
-
},
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
183
|
+
}],
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
assert(
|
|
187
|
+
["file", "command", "artifact"].every((category) => researchReportLedger.categories.includes(category)),
|
|
188
|
+
"a completed and audited deep-research report did not satisfy file, validation-command, and artifact evidence categories"
|
|
189
|
+
);
|
|
190
|
+
assert(
|
|
191
|
+
researchReportLedger.items.some(
|
|
192
|
+
(item) => item.category === "command" && /deterministic audit completed/.test(item.proof)
|
|
193
|
+
),
|
|
194
|
+
"deep-research validation evidence did not preserve its deterministic audit provenance"
|
|
195
|
+
);
|
|
196
|
+
} finally {
|
|
197
|
+
fs.rmSync(researchEvidenceRoot, { recursive: true, force: true });
|
|
198
|
+
}
|
|
187
199
|
|
|
188
200
|
const recoverableLedger = buildScsEvidenceLedger({
|
|
189
201
|
context: {
|
|
@@ -233,6 +233,29 @@ try {
|
|
|
233
233
|
"same-task resume used the new-request boundary marker"
|
|
234
234
|
);
|
|
235
235
|
|
|
236
|
+
const prefixedSameTaskContinuation = await runCase({
|
|
237
|
+
id: "ordinary-explanation",
|
|
238
|
+
goal: [
|
|
239
|
+
"You have explicit trusted-host approval for this isolated fixture.",
|
|
240
|
+
"Continue the same task from the current edits. Finish it with verified evidence.",
|
|
241
|
+
].join(" "),
|
|
242
|
+
resume: true,
|
|
243
|
+
responses: [assistant("A base case remains the condition that terminates recursive expansion.")],
|
|
244
|
+
});
|
|
245
|
+
const prefixedContinuationEvent = [...prefixedSameTaskContinuation.events]
|
|
246
|
+
.reverse()
|
|
247
|
+
.find((event) => event.type === "conversation.continued");
|
|
248
|
+
assert.equal(
|
|
249
|
+
prefixedContinuationEvent?.data?.preservesTaskBoundary,
|
|
250
|
+
true,
|
|
251
|
+
"a same-task continuation prefixed by a permission statement opened a new task boundary"
|
|
252
|
+
);
|
|
253
|
+
assert.equal(
|
|
254
|
+
prefixedSameTaskContinuation.state.meta?.goalContract?.taskGoal,
|
|
255
|
+
"Explain why recursion needs a base case.",
|
|
256
|
+
"a prefixed same-task continuation replaced the durable task goal"
|
|
257
|
+
);
|
|
258
|
+
|
|
236
259
|
const quotedChatClassification = await runCase({
|
|
237
260
|
id: "quoted-chat-classification",
|
|
238
261
|
taskProfile: "chatops",
|
|
@@ -266,6 +289,36 @@ try {
|
|
|
266
289
|
assert.equal(proseOnlyAction.events.filter((event) => event.type === "completion.evidence_rejected").length, 2);
|
|
267
290
|
assert(!proseOnlyAction.events.some((event) => event.type === "session.finished"));
|
|
268
291
|
|
|
292
|
+
const permissionPause = await runCase({
|
|
293
|
+
id: "permission-pause",
|
|
294
|
+
goal: "Run the checked-in project test script and report its verified result.",
|
|
295
|
+
taskProfile: "java",
|
|
296
|
+
allowShellTool: true,
|
|
297
|
+
scsActive: true,
|
|
298
|
+
setup: async (workspace) => {
|
|
299
|
+
await fs.mkdir(path.join(workspace, "scripts"), { recursive: true });
|
|
300
|
+
await fs.writeFile(path.join(workspace, "scripts", "test.sh"), "#!/usr/bin/env bash\necho pass\n", "utf8");
|
|
301
|
+
},
|
|
302
|
+
responses: [
|
|
303
|
+
assistant("", [toolCall("permission-test", "run_command", { command: "bash scripts/test.sh" })]),
|
|
304
|
+
],
|
|
305
|
+
});
|
|
306
|
+
assert.equal(permissionPause.calls.length, 1, "permission blocker consumed another model turn");
|
|
307
|
+
assert.equal(permissionPause.result.stopped, true);
|
|
308
|
+
assert.equal(permissionPause.result.reason, "permission_required");
|
|
309
|
+
assert(permissionPause.result.permissionAdvice?.suggestedCommand, "permission pause lost its exact resume command");
|
|
310
|
+
assert.equal(
|
|
311
|
+
permissionPause.events.filter((event) => event.type === "session.stopped" && event.data?.reason === "permission_required").length,
|
|
312
|
+
1,
|
|
313
|
+
"permission blocker did not persist exactly one paused state"
|
|
314
|
+
);
|
|
315
|
+
assert(
|
|
316
|
+
!permissionPause.events.some((event) =>
|
|
317
|
+
["scs.student.rethink_plan", "scs.student.reject_phase", "scs.committee.replan_drafted"].includes(event.type)
|
|
318
|
+
),
|
|
319
|
+
"permission blocker triggered an SCS replan instead of waiting for approval"
|
|
320
|
+
);
|
|
321
|
+
|
|
269
322
|
const reasoningTruncation = await runCase({
|
|
270
323
|
id: "reasoning-only-tool-continuation",
|
|
271
324
|
goal: "Run pwd and report the verified working directory.",
|
|
@@ -371,12 +424,12 @@ try {
|
|
|
371
424
|
|
|
372
425
|
const approvalNarrativeWithBlockerEvidence = await runCase({
|
|
373
426
|
id: "approval-narrative-with-blocker-evidence",
|
|
374
|
-
goal: "Run definitely_missing_aginti_command and report the result.",
|
|
427
|
+
goal: "Run which definitely_missing_aginti_command and report the result.",
|
|
375
428
|
taskProfile: "shell",
|
|
376
429
|
allowShellTool: true,
|
|
377
430
|
responses: [
|
|
378
431
|
assistant("", [
|
|
379
|
-
toolCall("missing-command", "run_command", { command: "definitely_missing_aginti_command" }),
|
|
432
|
+
toolCall("missing-command", "run_command", { command: "which definitely_missing_aginti_command" }),
|
|
380
433
|
]),
|
|
381
434
|
assistant("The command is unavailable. Approve installing it and I will continue after approval."),
|
|
382
435
|
assistant("Unable to execute the requested command because it is not installed in this environment."),
|
|
@@ -695,11 +748,11 @@ try {
|
|
|
695
748
|
|
|
696
749
|
const verifiedBlocker = await runCase({
|
|
697
750
|
id: "verified-blocker",
|
|
698
|
-
goal: "
|
|
751
|
+
goal: "Run which definitely_not_an_aginti_command and report the result.",
|
|
699
752
|
taskProfile: "shell",
|
|
700
753
|
allowShellTool: true,
|
|
701
754
|
responses: [
|
|
702
|
-
assistant("", [toolCall("run-blocked", "run_command", { command: "definitely_not_an_aginti_command" })]),
|
|
755
|
+
assistant("", [toolCall("run-blocked", "run_command", { command: "which definitely_not_an_aginti_command" })]),
|
|
703
756
|
assistant("", [
|
|
704
757
|
toolCall("finish-blocked", "finish", {
|
|
705
758
|
result: "Unable to execute the requested command because it is not installed in this environment.",
|
package/scripts/smoke-web-api.js
CHANGED
|
@@ -64,11 +64,12 @@ async function waitForHealth() {
|
|
|
64
64
|
throw new Error(`web server did not become healthy. stdout=${stdout.slice(-500)} stderr=${stderr.slice(-500)}`);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
async function waitForRun(sessionId) {
|
|
67
|
+
async function waitForRun(sessionId, terminalStatuses = ["finished", "failed"]) {
|
|
68
|
+
const acceptedStatuses = new Set(terminalStatuses);
|
|
68
69
|
const deadline = Date.now() + 20000;
|
|
69
70
|
while (Date.now() < deadline) {
|
|
70
71
|
const run = await fetchJson(`/api/runs/${encodeURIComponent(sessionId)}`);
|
|
71
|
-
if (
|
|
72
|
+
if (acceptedStatuses.has(run.status)) return run;
|
|
72
73
|
await delay(400);
|
|
73
74
|
}
|
|
74
75
|
throw new Error(`run ${sessionId} did not finish in time`);
|
|
@@ -548,7 +549,10 @@ try {
|
|
|
548
549
|
headless: true,
|
|
549
550
|
}),
|
|
550
551
|
});
|
|
551
|
-
const approvalRaceBlocked = await waitForRun(approvalRaceStart.sessionId);
|
|
552
|
+
const approvalRaceBlocked = await waitForRun(approvalRaceStart.sessionId, ["stopped", "failed"]);
|
|
553
|
+
if (approvalRaceBlocked.status !== "stopped") {
|
|
554
|
+
throw new Error(`permission/message race fixture did not stop for approval: ${approvalRaceBlocked.status}`);
|
|
555
|
+
}
|
|
552
556
|
if (!approvalRaceBlocked.logs?.some((entry) => entry.message === "tool.blocked" && entry.data?.permissionAdvice)) {
|
|
553
557
|
throw new Error("permission/message race fixture did not produce pending permission advice");
|
|
554
558
|
}
|
|
@@ -595,7 +599,10 @@ try {
|
|
|
595
599
|
headless: true,
|
|
596
600
|
}),
|
|
597
601
|
});
|
|
598
|
-
const safeRun = await waitForRun(safeRunStart.sessionId);
|
|
602
|
+
const safeRun = await waitForRun(safeRunStart.sessionId, ["stopped", "failed"]);
|
|
603
|
+
if (safeRun.status !== "stopped") {
|
|
604
|
+
throw new Error(`safe mode web run did not stop for approval: ${safeRun.status}`);
|
|
605
|
+
}
|
|
599
606
|
if (!safeRun.logs?.some((entry) => entry.message === "tool.blocked" && entry.data?.permissionAdvice?.category === "workspace-write")) {
|
|
600
607
|
throw new Error("safe mode web run did not expose workspace-write permission advice");
|
|
601
608
|
}
|
|
@@ -630,6 +637,14 @@ try {
|
|
|
630
637
|
if (!safeApproved.includes("Created by AgInTiFlow mock mode.")) {
|
|
631
638
|
throw new Error("permission-approved continuation did not create the requested file");
|
|
632
639
|
}
|
|
640
|
+
const staleApproval = await fetch(`${baseUrl}/api/sessions/${encodeURIComponent(safeRunStart.sessionId)}/approve-permission`, {
|
|
641
|
+
method: "POST",
|
|
642
|
+
headers: { "Content-Type": "application/json" },
|
|
643
|
+
body: JSON.stringify({ action: "once" }),
|
|
644
|
+
});
|
|
645
|
+
if (staleApproval.status !== 404) {
|
|
646
|
+
throw new Error(`resolved permission advice remained reusable: ${staleApproval.status}`);
|
|
647
|
+
}
|
|
633
648
|
await fetchJson("/api/preferences", {
|
|
634
649
|
method: "POST",
|
|
635
650
|
headers: { "Content-Type": "application/json" },
|
package/src/agent-runner.js
CHANGED
|
@@ -2216,7 +2216,7 @@ function isGenericTaskContinuationText(value = "") {
|
|
|
2216
2216
|
const normalized = String(value || "").replace(/\s+/g, " ").trim();
|
|
2217
2217
|
if (!normalized || normalized.length > 600) return false;
|
|
2218
2218
|
const explicitSameTaskContinuation =
|
|
2219
|
-
|
|
2219
|
+
/(?:^|[.!?]\s+)(?:(?:please|kindly)\s+)?(?:continue|resume|keep\s+working|finish|complete)\b.{0,180}\b(?:same|current|previous|existing|retained|saved|unfinished)\b.{0,80}\b(?:task|work|run|session|job|state)\b/i.test(normalized);
|
|
2220
2220
|
return explicitSameTaskContinuation || /^(?:(?:please|kindly)\s+)?(?:continue|resume|finish|complete|keep\s+working)(?:\s+(?:and\s+)?(?:continue|finish|complete|working))?(?:\s+(?:the\s+)?(?:same|current|previous|existing|retained|saved|unfinished)\s+(?:task|work|run|session|job))?(?:\s+from\s+(?:the\s+)?(?:retained|saved|current|previous)\s+state)?[.!?]*$/i.test(normalized) ||
|
|
2221
2221
|
/^(?:请)?(?:继续|接着|恢复|完成)(?:之前|上次|当前|同一|这个)?(?:的)?(?:任务|工作|会话|进度)?(?:并完成)?[。!?.!?]*$/u.test(normalized) ||
|
|
2222
2222
|
/^(?:このまま|前回から|保存した状態から)?(?:同じ|現在の|前の)?(?:タスク|作業|セッション)?(?:を)?(?:続けて|再開して|完了して)(?:ください)?[。!?.!?]*$/u.test(normalized);
|
|
@@ -2423,6 +2423,28 @@ async function finishWithDirectAnswer({ config, state, store, observers, session
|
|
|
2423
2423
|
};
|
|
2424
2424
|
}
|
|
2425
2425
|
|
|
2426
|
+
export function resetGoalScopedRuntimeState(state = {}) {
|
|
2427
|
+
state.meta = state.meta || {};
|
|
2428
|
+
const keys = [
|
|
2429
|
+
"artifactProgress",
|
|
2430
|
+
"completionEvidenceRepair",
|
|
2431
|
+
"dataProjectWorkflow",
|
|
2432
|
+
"durableEvidenceCategories",
|
|
2433
|
+
"durableGitActions",
|
|
2434
|
+
"durableGitEvidence",
|
|
2435
|
+
"failedTestRecoveryPacket",
|
|
2436
|
+
"projectVerification",
|
|
2437
|
+
"scs",
|
|
2438
|
+
];
|
|
2439
|
+
const removed = [];
|
|
2440
|
+
for (const key of keys) {
|
|
2441
|
+
if (!(key in state.meta)) continue;
|
|
2442
|
+
delete state.meta[key];
|
|
2443
|
+
removed.push(key);
|
|
2444
|
+
}
|
|
2445
|
+
return removed;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2426
2448
|
async function applyContinuationPrompt(state, config, observers) {
|
|
2427
2449
|
if (!config.resume || !config.goal) return null;
|
|
2428
2450
|
|
|
@@ -2439,6 +2461,9 @@ async function applyContinuationPrompt(state, config, observers) {
|
|
|
2439
2461
|
state.meta = state.meta || {};
|
|
2440
2462
|
const preserveTaskBoundary = preservesCurrentTaskBoundary(state, config.goal);
|
|
2441
2463
|
const goalUpdate = updateGoalContract(state, config.goal, { preserveTaskBoundary });
|
|
2464
|
+
if (!preserveTaskBoundary) {
|
|
2465
|
+
resetGoalScopedRuntimeState(state);
|
|
2466
|
+
}
|
|
2442
2467
|
if (
|
|
2443
2468
|
preserveTaskBoundary &&
|
|
2444
2469
|
continuationAddsConcreteRequirement(config.goal) &&
|
|
@@ -2882,6 +2907,14 @@ export function shouldShortCircuitToolBatch(toolResult) {
|
|
|
2882
2907
|
);
|
|
2883
2908
|
}
|
|
2884
2909
|
|
|
2910
|
+
export function shouldPauseForPermissionAdvice(toolResult = {}) {
|
|
2911
|
+
return Boolean(
|
|
2912
|
+
toolResult?.blocked &&
|
|
2913
|
+
toolResult?.permissionAdvice &&
|
|
2914
|
+
toolResult.permissionAdvice.autoRecover !== true
|
|
2915
|
+
);
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2885
2918
|
export function skippedAfterBlockedToolResult(toolCall, blockedResult) {
|
|
2886
2919
|
const toolName = toolCall?.function?.name || "unknown";
|
|
2887
2920
|
const args = sanitizeToolArgs(toolName, safeParseToolArgs(toolCall));
|
|
@@ -2961,6 +2994,19 @@ function commandReportsTestFailure(result = {}) {
|
|
|
2961
2994
|
);
|
|
2962
2995
|
}
|
|
2963
2996
|
|
|
2997
|
+
function explicitExitProbeStatus(command = "", result = {}) {
|
|
2998
|
+
const normalizedCommand = normalizeProjectCommand(command);
|
|
2999
|
+
const hasExitProbe = /(?:^|;)\s*(?:echo|printf)\b[^;&|]*(?:EXIT|STATUS|RESULT)[^;&|]*\$\?[^;&|]*$/i.test(
|
|
3000
|
+
normalizedCommand
|
|
3001
|
+
);
|
|
3002
|
+
if (!hasExitProbe) return { present: false, status: null };
|
|
3003
|
+
|
|
3004
|
+
const output = `${String(result.stdout || "")}\n${String(result.stderr || "")}`;
|
|
3005
|
+
const matches = [...output.matchAll(/(?:^|\n)\s*(?:EXIT|STATUS|RESULT)(?:_CODE)?\s*[:=]\s*(-?\d+)\s*(?=\n|$)/gim)];
|
|
3006
|
+
if (!matches.length) return { present: true, status: null };
|
|
3007
|
+
return { present: true, status: Number(matches.at(-1)[1]) };
|
|
3008
|
+
}
|
|
3009
|
+
|
|
2964
3010
|
function actionableTestWarnings(result = {}) {
|
|
2965
3011
|
const output = redactSensitiveText(`${String(result.stderr || "")}\n${String(result.stdout || "")}`);
|
|
2966
3012
|
const warnings = [];
|
|
@@ -3133,17 +3179,24 @@ export function recordProjectVerificationOutcome(state = {}, toolResult = {}, co
|
|
|
3133
3179
|
|
|
3134
3180
|
if (toolName === "run_command") {
|
|
3135
3181
|
const command = normalizeProjectCommand(toolResult.args?.command || "");
|
|
3182
|
+
const exitProbe = explicitExitProbeStatus(command, toolResult);
|
|
3136
3183
|
const run = {
|
|
3137
3184
|
command,
|
|
3138
3185
|
at: now,
|
|
3139
|
-
ok:
|
|
3186
|
+
ok:
|
|
3187
|
+
toolResult.ok !== false &&
|
|
3188
|
+
Number(toolResult.exitCode ?? 0) === 0 &&
|
|
3189
|
+
(!exitProbe.present || exitProbe.status === 0),
|
|
3140
3190
|
mutationRevision: verification.mutationRevision,
|
|
3191
|
+
...(exitProbe.present ? { explicitExitStatus: exitProbe.status } : {}),
|
|
3141
3192
|
};
|
|
3142
3193
|
verification.commandRuns = [...verification.commandRuns, run].slice(-40);
|
|
3143
3194
|
toolResult.projectMutationRevision = verification.mutationRevision;
|
|
3144
3195
|
if (isSubstantiveTestCommand(command)) {
|
|
3145
3196
|
const zeroTests = commandReportsZeroTests(toolResult);
|
|
3146
|
-
const reportedFailure =
|
|
3197
|
+
const reportedFailure =
|
|
3198
|
+
commandReportsTestFailure(toolResult) ||
|
|
3199
|
+
(exitProbe.present && exitProbe.status !== 0);
|
|
3147
3200
|
const qualityWarnings = actionableTestWarnings(toolResult);
|
|
3148
3201
|
const passed = run.ok && !zeroTests && !reportedFailure && qualityWarnings.length === 0;
|
|
3149
3202
|
const failedEvidence = passed ? {} : compactFailedTestEvidence(toolResult, config);
|
|
@@ -6396,6 +6449,51 @@ async function stopForMissingCompletionEvidence({ config, state, store, observer
|
|
|
6396
6449
|
};
|
|
6397
6450
|
}
|
|
6398
6451
|
|
|
6452
|
+
async function stopForPermissionAdvice({ config, state, store, observers, sessionId, step, toolResult }) {
|
|
6453
|
+
const advice = toolResult?.permissionAdvice && typeof toolResult.permissionAdvice === "object"
|
|
6454
|
+
? toolResult.permissionAdvice
|
|
6455
|
+
: {};
|
|
6456
|
+
const result = [
|
|
6457
|
+
advice.summary || toolResult?.reason || "The requested action needs a stronger permission mode.",
|
|
6458
|
+
advice.instruction || "Resume after approving the required mode or choose a safer alternative.",
|
|
6459
|
+
advice.suggestedCommand ? `Contained resume: ${advice.suggestedCommand}` : "",
|
|
6460
|
+
advice.trustedHostCommand ? `Trusted-host resume: ${advice.trustedHostCommand}` : "",
|
|
6461
|
+
].filter(Boolean).join("\n");
|
|
6462
|
+
const detail = {
|
|
6463
|
+
step,
|
|
6464
|
+
toolName: toolResult?.toolName || "",
|
|
6465
|
+
category: toolResult?.category || advice.category || "permission-required",
|
|
6466
|
+
reason: toolResult?.reason || advice.reason || "",
|
|
6467
|
+
permissionAdvice: advice,
|
|
6468
|
+
};
|
|
6469
|
+
state.stepsCompleted = step;
|
|
6470
|
+
state.updatedAt = new Date().toISOString();
|
|
6471
|
+
state.meta = state.meta || {};
|
|
6472
|
+
state.meta.pendingPermissionAdvice = detail;
|
|
6473
|
+
updateGoalStatus(state, "paused", "permission_required", state.updatedAt);
|
|
6474
|
+
await store.appendEvent("session.stopped", {
|
|
6475
|
+
reason: "permission_required",
|
|
6476
|
+
step,
|
|
6477
|
+
detail,
|
|
6478
|
+
});
|
|
6479
|
+
observers.event("session.stopped", {
|
|
6480
|
+
reason: "permission_required",
|
|
6481
|
+
sessionId,
|
|
6482
|
+
toolName: detail.toolName,
|
|
6483
|
+
category: detail.category,
|
|
6484
|
+
});
|
|
6485
|
+
await store.saveState(state);
|
|
6486
|
+
emitConsole(config, result, { kind: "error", error: true });
|
|
6487
|
+
return {
|
|
6488
|
+
sessionId,
|
|
6489
|
+
result,
|
|
6490
|
+
stopped: true,
|
|
6491
|
+
reason: "permission_required",
|
|
6492
|
+
permissionAdvice: advice,
|
|
6493
|
+
...goalRunMetadata(state),
|
|
6494
|
+
};
|
|
6495
|
+
}
|
|
6496
|
+
|
|
6399
6497
|
export function resetPerTurnToolContractState(state = {}, at = new Date().toISOString()) {
|
|
6400
6498
|
const prior = state.meta?.toolContractViolation;
|
|
6401
6499
|
if (!prior) return null;
|
|
@@ -7995,6 +8093,7 @@ export async function runAgent(config) {
|
|
|
7995
8093
|
|
|
7996
8094
|
let continueForQueuedInput = false;
|
|
7997
8095
|
let continueForCompletionRepair = false;
|
|
8096
|
+
let pendingPermissionPause = null;
|
|
7998
8097
|
const postBatchToolResults = [];
|
|
7999
8098
|
for (let toolIndex = 0; toolIndex < toolCalls.length; toolIndex += 1) {
|
|
8000
8099
|
const toolCall = toolCalls[toolIndex];
|
|
@@ -8083,6 +8182,7 @@ export async function runAgent(config) {
|
|
|
8083
8182
|
priorBlockedTool: skippedResult.priorBlockedTool,
|
|
8084
8183
|
});
|
|
8085
8184
|
}
|
|
8185
|
+
if (shouldPauseForPermissionAdvice(toolResult)) pendingPermissionPause = toolResult;
|
|
8086
8186
|
break;
|
|
8087
8187
|
}
|
|
8088
8188
|
|
|
@@ -8211,6 +8311,18 @@ export async function runAgent(config) {
|
|
|
8211
8311
|
}
|
|
8212
8312
|
}
|
|
8213
8313
|
|
|
8314
|
+
if (pendingPermissionPause) {
|
|
8315
|
+
return await stopForPermissionAdvice({
|
|
8316
|
+
config,
|
|
8317
|
+
state,
|
|
8318
|
+
store,
|
|
8319
|
+
observers,
|
|
8320
|
+
sessionId,
|
|
8321
|
+
step,
|
|
8322
|
+
toolResult: pendingPermissionPause,
|
|
8323
|
+
});
|
|
8324
|
+
}
|
|
8325
|
+
|
|
8214
8326
|
if (continueForCompletionRepair) continue;
|
|
8215
8327
|
|
|
8216
8328
|
for (const toolResult of postBatchToolResults) {
|
package/src/model-client.js
CHANGED
|
@@ -85,7 +85,11 @@ function mockToolCall(name, args = {}) {
|
|
|
85
85
|
|
|
86
86
|
function latestToolPayload(messages) {
|
|
87
87
|
for (const message of [...messages].reverse()) {
|
|
88
|
-
|
|
88
|
+
const userContent = String(message.content || "");
|
|
89
|
+
if (
|
|
90
|
+
message.role === "user" &&
|
|
91
|
+
/^(?:Continue with this new request:|Continue the current task from saved state:|Goal:)/i.test(userContent)
|
|
92
|
+
) {
|
|
89
93
|
return null;
|
|
90
94
|
}
|
|
91
95
|
if (message.role !== "tool" || !message.content) continue;
|
package/src/scs-evidence.js
CHANGED
|
@@ -487,6 +487,31 @@ function codeProfileRequiresCommand(goal = "") {
|
|
|
487
487
|
return substantiveCodeWork && !simpleDocumentWrite;
|
|
488
488
|
}
|
|
489
489
|
|
|
490
|
+
function goalRequestsWorkspaceMutation(goal = "") {
|
|
491
|
+
const text = normalizedText(goal);
|
|
492
|
+
return (
|
|
493
|
+
/\b(?:append|build|convert|copy|create|delete|edit|fix|generate|implement|modify|move|patch|refactor|remove|rename|repair|replace|rewrite|save|update|write)\b/.test(
|
|
494
|
+
text
|
|
495
|
+
) ||
|
|
496
|
+
/创建|写入|编辑|修复|实现|修改|更新|生成|保存|复制|移动|转换|删除|重命名|替换|追加/.test(text)
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function goalRequestsFileMutation(goal = "") {
|
|
501
|
+
const text = normalizedText(goal);
|
|
502
|
+
if (!goalRequestsWorkspaceMutation(text)) return false;
|
|
503
|
+
return (
|
|
504
|
+
/\b(?:code|codebase|document(?:ation)?|file|notes?|path|readme|repo(?:sitory)?|script|source|workspace)\b/.test(
|
|
505
|
+
text
|
|
506
|
+
) ||
|
|
507
|
+
/(?:^|[\s`'"(])(?:\.{0,2}\/|\/)?[a-z0-9_.-]+(?:\/[a-z0-9_.{}-]+)+/i.test(text) ||
|
|
508
|
+
/\.(?:c|cc|cpp|cs|css|csv|go|h|hpp|html?|java|js|jsx|json|kt|md|mjs|php|py|rb|rs|sh|swift|tex|ts|tsx|txt|ya?ml)\b/i.test(
|
|
509
|
+
text
|
|
510
|
+
) ||
|
|
511
|
+
/文件|文档|代码|代码库|仓库|脚本|源码|路径|工作区|说明书|笔记/.test(text)
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
490
515
|
function profileRequirementsForGoal(taskProfile = "", goal = "") {
|
|
491
516
|
const profile = String(taskProfile || "").toLowerCase();
|
|
492
517
|
const defaults = PROFILE_REQUIREMENTS[profile] || [];
|
|
@@ -510,8 +535,14 @@ function profileRequirementsForGoal(taskProfile = "", goal = "") {
|
|
|
510
535
|
"aaps",
|
|
511
536
|
]);
|
|
512
537
|
if (!codeLikeProfiles.has(profile)) return defaults;
|
|
513
|
-
|
|
514
|
-
|
|
538
|
+
let requirements = [...defaults];
|
|
539
|
+
if (!goalRequestsWorkspaceMutation(goal)) {
|
|
540
|
+
requirements = requirements.filter((category) => category !== "file");
|
|
541
|
+
}
|
|
542
|
+
if (!codeProfileRequiresCommand(goal)) {
|
|
543
|
+
requirements = requirements.filter((category) => category !== "command");
|
|
544
|
+
}
|
|
545
|
+
return requirements;
|
|
515
546
|
}
|
|
516
547
|
|
|
517
548
|
function isReadOnlyReadinessTask(goal = "") {
|
|
@@ -549,19 +580,21 @@ function inferRequirementCategories(goal = "", taskProfile = "", acceptanceCrite
|
|
|
549
580
|
/\b(?:clean(?:\s+up)?|remove|delete|clear|purge)\b[^.\n;]{0,120}\b(?:generated|temporary|stale|test)?\s*(?:test\s+)?(?:debris|caches?|byproducts?)\b/gi,
|
|
550
581
|
""
|
|
551
582
|
)
|
|
583
|
+
.replace(
|
|
584
|
+
/\b(?:ignore|exclude|omit|skip|leave\s+out)\b[^.\n;]{0,160}\b(?:generated|temporary|stale|build|session)\b[^.\n;]{0,100}\b(?:outputs?|artifacts?|files?|directories?|folders?)\b/gi,
|
|
585
|
+
""
|
|
586
|
+
)
|
|
552
587
|
.replace(/\bfigure\s+out\b/gi, "");
|
|
588
|
+
const mandatoryEvidenceText = artifactSignalText.replace(
|
|
589
|
+
/[^.\n]{0,240}\b(?:as appropriate|if appropriate|when useful|where applicable)\b/gi,
|
|
590
|
+
" "
|
|
591
|
+
);
|
|
553
592
|
const profile = String(taskProfile || "").toLowerCase();
|
|
554
593
|
const categories = new Set(
|
|
555
594
|
goalRequiresEvidence(positiveGoal, "") ? profileRequirementsForGoal(taskProfile, positiveGoal) : []
|
|
556
595
|
);
|
|
557
596
|
|
|
558
|
-
if (
|
|
559
|
-
textHas(
|
|
560
|
-
text,
|
|
561
|
-
/\b(file|path|workspace|edit|patch|fix|repair|refactor|convert|copy|move|remove|delete|source|script|code)\b|\.(?:md|txt|js|jsx|ts|tsx|mjs|cjs|json|ya?ml|py|tex|html|css|svg|csv)\b|\b(?:markdown|json|yaml|html|css|tex|latex)\s+file\b|\bfile\s+(?:as|in)\s+(?:markdown|json|yaml|html|css|tex|latex)\b/
|
|
562
|
-
) ||
|
|
563
|
-
/文件|写入文件|编辑|修复|转换|复制|移动|删除|脚本|代码/.test(text)
|
|
564
|
-
) {
|
|
597
|
+
if (goalRequestsFileMutation(positiveGoal)) {
|
|
565
598
|
categories.add("file");
|
|
566
599
|
}
|
|
567
600
|
const directCommandSignal =
|
|
@@ -577,13 +610,13 @@ function inferRequirementCategories(goal = "", taskProfile = "", acceptanceCrite
|
|
|
577
610
|
if (directCommandSignal || (validationSignal && codeProfileRequiresCommand(positiveGoal))) {
|
|
578
611
|
categories.add("command");
|
|
579
612
|
}
|
|
580
|
-
if (textHas(
|
|
613
|
+
if (textHas(mandatoryEvidenceText, /\b(artifact|canvas|pdf|image|video|screenshot|cover|plot|chart|figure|docx|archive|copy to|export|generated|generate|draft)\b/) || /输出|产物|图片|视频|截图|封面|生成/.test(mandatoryEvidenceText)) {
|
|
581
614
|
categories.add("artifact");
|
|
582
615
|
}
|
|
583
|
-
if (textHas(
|
|
616
|
+
if (textHas(mandatoryEvidenceText, /\b(browser|chrome|chromium|cdp|devtools|playwright|selenium|web[- ]?ui|website|page|tab|composer|click|type|upload|attach|submit|form)\b/) || /浏览器|网页|页面|上传|提交|附件|资产库/.test(mandatoryEvidenceText)) {
|
|
584
617
|
categories.add("browser");
|
|
585
618
|
}
|
|
586
|
-
if (textHas(
|
|
619
|
+
if (textHas(mandatoryEvidenceText, /\b(screenshot|visible|visual|see|inspect image|open image|read_image|thumbnail)\b/) || /截图|可见|缩略图/.test(mandatoryEvidenceText)) {
|
|
587
620
|
categories.add("visual");
|
|
588
621
|
}
|
|
589
622
|
if (
|
|
@@ -2143,6 +2176,7 @@ function eventToEvidence(event = {}) {
|
|
|
2143
2176
|
target: data.path || data.artifactId || data.outputPath || "",
|
|
2144
2177
|
proof: type,
|
|
2145
2178
|
verified: true,
|
|
2179
|
+
virtualArtifact: Boolean(data.artifactId && !data.path && !data.outputPath),
|
|
2146
2180
|
});
|
|
2147
2181
|
if (type === "image.generated") {
|
|
2148
2182
|
evidence.push({
|
|
@@ -2251,16 +2285,31 @@ function toolPayloadToEvidence(payload = {}, source = "tool") {
|
|
|
2251
2285
|
if (["open_url", "click", "type", "scroll", "press", "back"].includes(toolName) || /\b(browser|chrome|cdp|playwright|selenium|upload|attach|submit|click|tab|page)\b/.test(text)) {
|
|
2252
2286
|
push("browser", `${toolName || "browser tool"} affected or inspected browser/UI state`, payload.url || args.url || args.command || "");
|
|
2253
2287
|
}
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2288
|
+
const artifactTools = new Set([
|
|
2289
|
+
"open_workspace_file",
|
|
2290
|
+
"preview_workspace",
|
|
2291
|
+
"send_to_canvas",
|
|
2292
|
+
"generate_image",
|
|
2293
|
+
"read_image",
|
|
2294
|
+
"writing_specialist",
|
|
2295
|
+
"json_specialist",
|
|
2296
|
+
"json_specialist_batch",
|
|
2297
|
+
]);
|
|
2298
|
+
const artifactPath = firstArtifactPath(
|
|
2299
|
+
payload.artifactPath,
|
|
2300
|
+
payload.outputPath,
|
|
2301
|
+
payload.reportPath,
|
|
2302
|
+
payload.path,
|
|
2303
|
+
args.path,
|
|
2304
|
+
text
|
|
2305
|
+
);
|
|
2306
|
+
if (artifactTools.has(toolName) || payload.artifactId || artifactPath) {
|
|
2307
|
+
push(
|
|
2308
|
+
"artifact",
|
|
2309
|
+
`${toolName || "tool"} produced or inspected an artifact`,
|
|
2310
|
+
artifactPath || payload.artifactId || "",
|
|
2311
|
+
{ virtualArtifact: Boolean(payload.artifactId && !artifactPath) }
|
|
2312
|
+
);
|
|
2264
2313
|
}
|
|
2265
2314
|
if (["read_image", "generate_image"].includes(toolName) || /\b(screenshot|visible|thumbnail|preview|image)\b/.test(text)) {
|
|
2266
2315
|
push("visual", `${toolName || "tool"} supplied visual evidence`, payload.path || payload.outputPath || args.path || "");
|
|
@@ -2284,6 +2333,59 @@ function toolPayloadToEvidence(payload = {}, source = "tool") {
|
|
|
2284
2333
|
return evidence;
|
|
2285
2334
|
}
|
|
2286
2335
|
|
|
2336
|
+
const ARTIFACT_EXTENSION_PATTERN = /\.(?:md|json|csv|txt|html?|tex|pdf|docx|pptx|xlsx|png|jpe?g|webp|svg|mp4|mov|mkv|webm|wav|mp3|flac|zip|7z|tar|gz|step|stp|stl|3mf)$/i;
|
|
2337
|
+
const ARTIFACT_PATH_PATTERN = /(?:^|[\s"'`(=])([^\s"'`()=]+\.(?:md|json|csv|txt|html?|tex|pdf|docx|pptx|xlsx|png|jpe?g|webp|svg|mp4|mov|mkv|webm|wav|mp3|flac|zip|7z|tar|gz|step|stp|stl|3mf))(?:$|[\s"'`),;:])/i;
|
|
2338
|
+
|
|
2339
|
+
function firstArtifactPath(...values) {
|
|
2340
|
+
for (const value of values) {
|
|
2341
|
+
const candidate = String(value || "").trim();
|
|
2342
|
+
if (!candidate) continue;
|
|
2343
|
+
if (!/\s/.test(candidate) && ARTIFACT_EXTENSION_PATTERN.test(candidate)) {
|
|
2344
|
+
return candidate;
|
|
2345
|
+
}
|
|
2346
|
+
const match = candidate.match(ARTIFACT_PATH_PATTERN);
|
|
2347
|
+
if (match?.[1]) return match[1];
|
|
2348
|
+
}
|
|
2349
|
+
return "";
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
function revalidateArtifactEvidence(item = {}, state = {}, context = {}) {
|
|
2353
|
+
if (item?.category !== "artifact" || item?.verified === false || item?.virtualArtifact === true) return item;
|
|
2354
|
+
const candidate = firstArtifactPath(item.target, item.proof);
|
|
2355
|
+
if (!candidate) {
|
|
2356
|
+
return item.toolName === "run_command"
|
|
2357
|
+
? {
|
|
2358
|
+
...item,
|
|
2359
|
+
verified: false,
|
|
2360
|
+
proof: `${item.proof || "artifact evidence"}; no durable artifact path was reported`,
|
|
2361
|
+
}
|
|
2362
|
+
: item;
|
|
2363
|
+
}
|
|
2364
|
+
const commandCwd = String(
|
|
2365
|
+
context.commandCwd ||
|
|
2366
|
+
state.commandCwd ||
|
|
2367
|
+
state.meta?.runtimeConfig?.commandCwd ||
|
|
2368
|
+
process.cwd()
|
|
2369
|
+
);
|
|
2370
|
+
const resolved = path.isAbsolute(candidate) ? candidate : path.resolve(commandCwd, candidate);
|
|
2371
|
+
let durable = false;
|
|
2372
|
+
try {
|
|
2373
|
+
const stat = fs.statSync(resolved);
|
|
2374
|
+
durable = stat.isDirectory() || (stat.isFile() && stat.size > 0);
|
|
2375
|
+
} catch {
|
|
2376
|
+
durable = false;
|
|
2377
|
+
}
|
|
2378
|
+
return {
|
|
2379
|
+
...item,
|
|
2380
|
+
target: candidate,
|
|
2381
|
+
resolvedTarget: resolved,
|
|
2382
|
+
verified: durable,
|
|
2383
|
+
proof: durable
|
|
2384
|
+
? item.proof
|
|
2385
|
+
: `${item.proof || "artifact evidence"}; artifact path no longer exists or is empty`,
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2287
2389
|
function messageToEvidence(message = {}) {
|
|
2288
2390
|
if (message.role !== "tool") return [];
|
|
2289
2391
|
try {
|
|
@@ -2298,9 +2400,12 @@ export function buildScsEvidenceLedger({ state = {}, context = {} } = {}) {
|
|
|
2298
2400
|
const messages = Array.isArray(state.messages) ? state.messages : [];
|
|
2299
2401
|
const eventEvidence = events.flatMap(eventToEvidence);
|
|
2300
2402
|
const messageEvidence = messages.flatMap(messageToEvidence);
|
|
2301
|
-
const items = [...eventEvidence, ...messageEvidence]
|
|
2302
|
-
|
|
2303
|
-
|
|
2403
|
+
const items = [...eventEvidence, ...messageEvidence]
|
|
2404
|
+
.slice(-80)
|
|
2405
|
+
.map((item) => revalidateArtifactEvidence(item, state, context));
|
|
2406
|
+
const verifiedItems = items.filter((item) => item?.verified !== false);
|
|
2407
|
+
const categories = unique(verifiedItems.map((item) => item.category));
|
|
2408
|
+
const toolNames = unique(verifiedItems.map((item) => item.toolName).filter(Boolean));
|
|
2304
2409
|
const blockers = [...events.map(eventToBlocker), ...messages.map(messageToBlocker)]
|
|
2305
2410
|
.filter(Boolean)
|
|
2306
2411
|
.slice(-20)
|
package/web.js
CHANGED
|
@@ -1367,6 +1367,17 @@ async function ensureNotRunning(sessionId) {
|
|
|
1367
1367
|
}
|
|
1368
1368
|
|
|
1369
1369
|
async function latestPermissionAdvice(sessionId) {
|
|
1370
|
+
const events = await sessionStore(sessionId).loadEvents().catch(() => []);
|
|
1371
|
+
for (const event of [...events].reverse()) {
|
|
1372
|
+
if (["permission.approval_granted", "permission.approval_declined"].includes(event.type)) return null;
|
|
1373
|
+
if (event.type === "tool.blocked" && event.data?.permissionAdvice) {
|
|
1374
|
+
return {
|
|
1375
|
+
...event.data.permissionAdvice,
|
|
1376
|
+
category: event.data.permissionAdvice.category || event.data.category || "",
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1370
1381
|
const inMemory = runs.get(sessionId);
|
|
1371
1382
|
const memoryEntry = [...(inMemory?.logs || [])]
|
|
1372
1383
|
.reverse()
|
|
@@ -1378,13 +1389,7 @@ async function latestPermissionAdvice(sessionId) {
|
|
|
1378
1389
|
};
|
|
1379
1390
|
}
|
|
1380
1391
|
|
|
1381
|
-
|
|
1382
|
-
const event = [...events].reverse().find((candidate) => candidate.type === "tool.blocked" && candidate.data?.permissionAdvice);
|
|
1383
|
-
if (!event?.data?.permissionAdvice) return null;
|
|
1384
|
-
return {
|
|
1385
|
-
...event.data.permissionAdvice,
|
|
1386
|
-
category: event.data.permissionAdvice.category || event.data.category || "",
|
|
1387
|
-
};
|
|
1392
|
+
return null;
|
|
1388
1393
|
}
|
|
1389
1394
|
|
|
1390
1395
|
function permissionApprovalPrompt(action, advice = {}, originalGoal = "") {
|
|
@@ -2421,6 +2426,12 @@ app.post("/api/sessions/:sessionId/approve-permission", async (req, res) => {
|
|
|
2421
2426
|
source: "web",
|
|
2422
2427
|
category: advice.category || "",
|
|
2423
2428
|
});
|
|
2429
|
+
const state = await store.loadState();
|
|
2430
|
+
if (state?.meta?.pendingPermissionAdvice) {
|
|
2431
|
+
delete state.meta.pendingPermissionAdvice;
|
|
2432
|
+
state.updatedAt = new Date().toISOString();
|
|
2433
|
+
await store.saveState(state);
|
|
2434
|
+
}
|
|
2424
2435
|
const existing = runs.get(sessionId);
|
|
2425
2436
|
if (existing) {
|
|
2426
2437
|
existing.logs.push({
|
|
@@ -2473,6 +2484,11 @@ app.post("/api/sessions/:sessionId/approve-permission", async (req, res) => {
|
|
|
2473
2484
|
category: advice.category || "",
|
|
2474
2485
|
permissionMode: targetMode,
|
|
2475
2486
|
});
|
|
2487
|
+
if (state.meta?.pendingPermissionAdvice) {
|
|
2488
|
+
delete state.meta.pendingPermissionAdvice;
|
|
2489
|
+
state.updatedAt = new Date().toISOString();
|
|
2490
|
+
await store.saveState(state);
|
|
2491
|
+
}
|
|
2476
2492
|
|
|
2477
2493
|
const stored = await loadStoredRun(sessionId);
|
|
2478
2494
|
if (runs.get(sessionId)?.status === "running") {
|