@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.
@@ -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.212",
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 presentWriterToolEval = evaluateScsEvidence(
1090
- requiredWriterToolContract,
1091
- buildScsEvidenceLedger({
1092
- context: {
1093
- events: [
1094
- { type: "file.changed", data: { path: "final/story.md" } },
1095
- { type: "tool.completed", data: { toolName: "writing_specialist", ok: true, artifactPath: "artifacts/writer.json" } },
1096
- ],
1097
- },
1098
- })
1099
- );
1100
- assert(presentWriterToolEval.ok, "SCS should accept required specialist call when tool evidence is present");
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 researchReportLedger = buildScsEvidenceLedger({
156
- context: {
157
- events: [{
158
- type: "tool.completed",
159
- data: {
160
- ok: true,
161
- toolName: "deep_research",
162
- status: "completed",
163
- reportPath: "/workspace/reports/cited-research.md",
164
- artifactPath: "/session/artifacts/deep-research.json",
165
- coverage: {
166
- verifiedClaimCount: 12,
167
- quoteVerificationRate: 1,
168
- },
169
- audit: {
170
- citationCoverage: 1,
171
- unknownEvidenceIds: [],
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
- assert(
178
- ["file", "command", "artifact"].every((category) => researchReportLedger.categories.includes(category)),
179
- "a completed and audited deep-research report did not satisfy file, validation-command, and artifact evidence categories"
180
- );
181
- assert(
182
- researchReportLedger.items.some(
183
- (item) => item.category === "command" && /deterministic audit completed/.test(item.proof)
184
- ),
185
- "deep-research validation evidence did not preserve its deterministic audit provenance"
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: "Execute the shell command definitely_not_an_aginti_command and report the result.",
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.",
@@ -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 (run.status === "finished" || run.status === "failed") return run;
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" },
@@ -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
- /^(?:(?: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);
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: toolResult.ok !== false && Number(toolResult.exitCode ?? 0) === 0,
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 = commandReportsTestFailure(toolResult);
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) {
@@ -85,7 +85,11 @@ function mockToolCall(name, args = {}) {
85
85
 
86
86
  function latestToolPayload(messages) {
87
87
  for (const message of [...messages].reverse()) {
88
- if (message.role === "user" && /^Continue with this new request:|^Goal:/i.test(String(message.content || ""))) {
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;
@@ -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
- if (codeProfileRequiresCommand(goal)) return defaults;
514
- return defaults.filter((category) => category !== "command");
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(artifactSignalText, /\b(artifact|canvas|pdf|image|video|screenshot|cover|plot|chart|figure|docx|archive|copy to|export|generated|generate|draft)\b/) || /输出|产物|图片|视频|截图|封面|生成/.test(artifactSignalText)) {
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(text, /\b(browser|chrome|chromium|cdp|devtools|playwright|selenium|web[- ]?ui|website|page|tab|composer|click|type|upload|attach|submit|form)\b/) || /浏览器|网页|页面|上传|提交|附件|资产库/.test(text)) {
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(text, /\b(screenshot|visible|visual|see|inspect image|open image|read_image|thumbnail)\b/) || /截图|可见|缩略图/.test(text)) {
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
- if (
2255
- ["open_workspace_file", "preview_workspace", "send_to_canvas", "generate_image", "read_image", "writing_specialist", "json_specialist", "json_specialist_batch"].includes(
2256
- toolName
2257
- ) ||
2258
- payload.artifactId ||
2259
- payload.outputPath ||
2260
- payload.artifactPath ||
2261
- /\b(pdf|png|jpg|jpeg|image|video|screenshot|artifact|preview)\b/.test(text)
2262
- ) {
2263
- push("artifact", `${toolName || "tool"} produced or inspected an artifact`, payload.path || payload.outputPath || payload.artifactPath || args.path || "");
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].slice(-80);
2302
- const categories = unique(items.map((item) => item.category));
2303
- const toolNames = unique(items.map((item) => item.toolName).filter(Boolean));
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
- const events = await sessionStore(sessionId).loadEvents().catch(() => []);
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") {