@tea-agent/loop-agent 0.16.25 → 0.16.26
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/CHANGELOG.md +9 -0
- package/dist/executors/dag-pi-executor.js +63 -9
- package/dist/executors/shell-executor.js +30 -0
- package/dist/executors/shell-write-guard.js +64 -2
- package/dist/worker/delivery/git-transaction.js +43 -8
- package/dist/worker/observe/paths.js +81 -0
- package/dist/worker/observe/routes.js +127 -19
- package/dist/worker/observe/spec-evidence.js +84 -0
- package/dist/worker/observe/static/api.js +23 -0
- package/dist/worker/observe/static/state.js +26 -0
- package/dist/worker/observe/static/styles.css +10 -0
- package/dist/worker/observe/static/views/dag-inspector.js +173 -6
- package/dist/workflows/dag/init-hybrid.js +104 -0
- package/dist/workflows/dag/node-execution.js +32 -5
- package/dist/workflows/dag/project-governance-context.js +508 -0
- package/dist/workflows/dag/prompt.js +46 -1
- package/dist/workflows/dag/skill-snapshot.js +1 -0
- package/dist/workflows/dag/types.js +10 -0
- package/dist/workflows/dag/validate.js +28 -0
- package/docs/templates/agent-dag.schema.json +15 -0
- package/docs/templates/agent-dag.supervised-implementation.json +1 -0
- package/package.json +1 -1
|
@@ -132,6 +132,90 @@ async function readSessionEvents(filePath) {
|
|
|
132
132
|
return [];
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Coerce a `tool_execution_end` result body (toolResult.content or result.content)
|
|
137
|
+
* into a string. Accepts plain strings, `{type:"text", text}[]` arrays, and
|
|
138
|
+
* message.content shapes. Returns null when no usable body is present.
|
|
139
|
+
*/
|
|
140
|
+
function resultContentToString(content) {
|
|
141
|
+
if (content == null)
|
|
142
|
+
return null;
|
|
143
|
+
if (typeof content === "string")
|
|
144
|
+
return content;
|
|
145
|
+
if (Array.isArray(content)) {
|
|
146
|
+
const parts = [];
|
|
147
|
+
for (const part of content) {
|
|
148
|
+
if (typeof part === "string") {
|
|
149
|
+
parts.push(part);
|
|
150
|
+
}
|
|
151
|
+
else if (part && typeof part === "object" && typeof part.text === "string") {
|
|
152
|
+
parts.push(part.text);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Scan session events for successful paired `read` tool calls and return the
|
|
161
|
+
* most recent result body + timestamp whose repo-relative path matches
|
|
162
|
+
* `relPath` exactly. Used for AC-002: show the content the model actually
|
|
163
|
+
* received at read time, never the current workspace file.
|
|
164
|
+
*/
|
|
165
|
+
export async function extractSpecReadContent(repoRoot, dagRunId, nodeId, relPath) {
|
|
166
|
+
if (!relPath || !isSafeObservabilityIdentifier(dagRunId) || !isSafeObservabilityIdentifier(nodeId)) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
// The preview endpoint must expose exactly the same membership as the
|
|
170
|
+
// `specReads` list, not every file the node happened to read.
|
|
171
|
+
if (!isSpecFilePath(relPath))
|
|
172
|
+
return null;
|
|
173
|
+
const eventsPath = resolveSessionEventsPath(repoRoot, dagRunId, nodeId);
|
|
174
|
+
if (!eventsPath)
|
|
175
|
+
return null;
|
|
176
|
+
const events = await readSessionEvents(eventsPath);
|
|
177
|
+
const startMap = new Map();
|
|
178
|
+
let latest = null;
|
|
179
|
+
for (const event of events) {
|
|
180
|
+
const type = event.type;
|
|
181
|
+
const toolName = resolveToolName(event);
|
|
182
|
+
const toolCallId = event.toolCallId;
|
|
183
|
+
if (type === "tool_execution_start" && toolCallId) {
|
|
184
|
+
startMap.set(toolCallId, { toolName, input: eventToolArgs(event) });
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (type !== "tool_execution_end" || toolName !== "read" || !toolCallId)
|
|
188
|
+
continue;
|
|
189
|
+
const start = startMap.get(toolCallId);
|
|
190
|
+
if (!start || start.toolName !== "read")
|
|
191
|
+
continue;
|
|
192
|
+
const isErrored = event.isError === true ||
|
|
193
|
+
event.toolResult?.error != null ||
|
|
194
|
+
event.toolResult?.ok === false ||
|
|
195
|
+
event.result?.error != null ||
|
|
196
|
+
event.result?.ok === false;
|
|
197
|
+
if (isErrored) {
|
|
198
|
+
startMap.delete(toolCallId);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const startInput = start.input ?? {};
|
|
202
|
+
const filePath = typeof startInput.path === "string" ? startInput.path : undefined;
|
|
203
|
+
if (!filePath) {
|
|
204
|
+
startMap.delete(toolCallId);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const rel = toRepoRelative(repoRoot, filePath);
|
|
208
|
+
startMap.delete(toolCallId);
|
|
209
|
+
if (!rel || rel !== relPath)
|
|
210
|
+
continue;
|
|
211
|
+
const body = resultContentToString(event.toolResult?.content) ??
|
|
212
|
+
resultContentToString(event.result?.content);
|
|
213
|
+
if (body == null)
|
|
214
|
+
continue;
|
|
215
|
+
latest = { content: body, timestamp: eventTimestamp(event) };
|
|
216
|
+
}
|
|
217
|
+
return latest;
|
|
218
|
+
}
|
|
135
219
|
export async function extractSpecEvidence(repoRoot, dagRunId, nodeId) {
|
|
136
220
|
if (!isSafeObservabilityIdentifier(dagRunId) || !isSafeObservabilityIdentifier(nodeId)) {
|
|
137
221
|
return null;
|
|
@@ -29,6 +29,29 @@ export async function fetchJsonResult(url) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Fetch a spec-evidence file preview (binding source or successful read).
|
|
34
|
+
* Resolves to the parsed JSON body, or throws an Error carrying the HTTP
|
|
35
|
+
* status and server message so the UI can render an accessible error state.
|
|
36
|
+
*/
|
|
37
|
+
export async function fetchSpecEvidenceFile(runId, nodeId, source, relPath) {
|
|
38
|
+
const url = `/api/dag-runs/${encodeURIComponent(runId)}/nodes/${encodeURIComponent(nodeId)}/spec-evidence/file?source=${encodeURIComponent(source)}&path=${encodeURIComponent(relPath)}`;
|
|
39
|
+
const res = await fetch(url);
|
|
40
|
+
let body = null;
|
|
41
|
+
try {
|
|
42
|
+
body = await res.json();
|
|
43
|
+
} catch {
|
|
44
|
+
body = null;
|
|
45
|
+
}
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
const message = body?.error || `加载失败:HTTP ${res.status}`;
|
|
48
|
+
const error = new Error(message);
|
|
49
|
+
error.status = res.status;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
return body;
|
|
53
|
+
}
|
|
54
|
+
|
|
32
55
|
export function artifactUrl(artifactPath) {
|
|
33
56
|
return `/api/artifacts?path=${encodeURIComponent(artifactPath)}&tail=200`;
|
|
34
57
|
}
|
|
@@ -46,6 +46,13 @@ export const uiState = {
|
|
|
46
46
|
dagTimelineViewportState: null,
|
|
47
47
|
dagNodeOutputViewportState: null,
|
|
48
48
|
runOutputViewportState: null,
|
|
49
|
+
/**
|
|
50
|
+
* Spec-evidence file preview drill-down. null = list view; otherwise
|
|
51
|
+
* { source, path, loading, error, data, triggerId } for the active preview.
|
|
52
|
+
*/
|
|
53
|
+
specEvidenceDetail: null,
|
|
54
|
+
/** Last-rendered spec-evidence list, used to restore the list synchronously. */
|
|
55
|
+
specEvidenceListEvidence: null,
|
|
49
56
|
pollingGeneration: 0,
|
|
50
57
|
};
|
|
51
58
|
|
|
@@ -271,3 +278,22 @@ export function bumpPollingGeneration() {
|
|
|
271
278
|
uiState.pollingGeneration += 1;
|
|
272
279
|
return uiState.pollingGeneration;
|
|
273
280
|
}
|
|
281
|
+
|
|
282
|
+
/** Open an in-inspector spec-evidence file preview (AC-001/002). */
|
|
283
|
+
export function openSpecEvidenceDetail(source, path, triggerEl) {
|
|
284
|
+
uiState.specEvidenceDetail = {
|
|
285
|
+
source,
|
|
286
|
+
path,
|
|
287
|
+
loading: true,
|
|
288
|
+
error: null,
|
|
289
|
+
data: null,
|
|
290
|
+
triggerId: triggerEl?.id ?? null,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Close the preview; caller restores focus to the trigger element. */
|
|
295
|
+
export function closeSpecEvidenceDetail() {
|
|
296
|
+
const detail = uiState.specEvidenceDetail;
|
|
297
|
+
uiState.specEvidenceDetail = null;
|
|
298
|
+
return detail;
|
|
299
|
+
}
|
|
@@ -1863,6 +1863,16 @@ body.is-resizing-dag-graph {
|
|
|
1863
1863
|
.spec-evidence-time { margin-left: auto; color: var(--muted); font-size: 10px; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
1864
1864
|
.spec-evidence-warning { display: flex; align-items: flex-start; gap: 8px; margin-top: var(--space-4); padding: 10px 12px; border: 1px solid color-mix(in srgb, var(--amber) 50%, var(--hairline)); border-radius: var(--radius-sm); background: #fff8e7; color: var(--body); font-size: 12px; line-height: 1.6; }
|
|
1865
1865
|
.spec-evidence-warning > i { flex-shrink: 0; margin-top: 2px; color: var(--amber); }
|
|
1866
|
+
.spec-evidence-file-btn { display: flex; align-items: flex-start; gap: 6px; width: 100%; padding: 4px 0; background: none; border: none; text-align: left; cursor: pointer; color: inherit; font: inherit; line-height: 1.5; overflow-wrap: anywhere; border-radius: var(--radius-sm); }
|
|
1867
|
+
.spec-evidence-file-btn:hover, .spec-evidence-file-btn:focus-visible { background: color-mix(in srgb, var(--accent, #2563eb) 8%, transparent); outline: none; }
|
|
1868
|
+
.spec-evidence-file-btn:focus-visible { box-shadow: 0 0 0 2px var(--accent, #2563eb); }
|
|
1869
|
+
.spec-evidence-detail { display: grid; gap: var(--space-3); }
|
|
1870
|
+
.spec-evidence-back { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border: 1px solid var(--hairline); border-radius: var(--radius-sm); background: var(--surface, #fff); color: var(--ink); font-size: 12px; cursor: pointer; }
|
|
1871
|
+
.spec-evidence-back:hover, .spec-evidence-back:focus-visible { background: color-mix(in srgb, var(--accent, #2563eb) 8%, var(--surface, #fff)); }
|
|
1872
|
+
.spec-evidence-detail-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 12px; color: var(--body); }
|
|
1873
|
+
.spec-evidence-detail-content { min-height: 0; max-width: 100%; margin: 0; padding: var(--space-3); border: 1px solid var(--hairline); border-radius: var(--radius-sm); background: var(--surface, #fff); white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12px; line-height: 1.6; }
|
|
1874
|
+
.spec-evidence-detail-error { margin: 0; color: var(--red, #c00); font-size: 12px; }
|
|
1875
|
+
.spec-evidence-detail-truncated { margin: 0; color: var(--muted); font-size: 11px; }
|
|
1866
1876
|
|
|
1867
1877
|
.run-layer-process {
|
|
1868
1878
|
display: grid;
|
|
@@ -10,12 +10,14 @@ import {
|
|
|
10
10
|
UI_TEXT,
|
|
11
11
|
} from "../constants.js";
|
|
12
12
|
import { clearNode, el } from "../dom.js";
|
|
13
|
-
import { fetchJson } from "../api.js";
|
|
13
|
+
import { fetchJson, fetchSpecEvidenceFile } from "../api.js";
|
|
14
14
|
import { badge } from "../format.js";
|
|
15
15
|
import { renderMarkdown } from "../markdown-render.js";
|
|
16
16
|
import {
|
|
17
17
|
uiState,
|
|
18
18
|
makeSessionEventIdentity,
|
|
19
|
+
openSpecEvidenceDetail,
|
|
20
|
+
closeSpecEvidenceDetail,
|
|
19
21
|
updateDagTimelineViewportState,
|
|
20
22
|
updateDagOutputViewportState,
|
|
21
23
|
computeFollowLatest,
|
|
@@ -44,6 +46,10 @@ export function selectDagNode(dagRunId, nodeId) {
|
|
|
44
46
|
if (nodeChanged) {
|
|
45
47
|
uiState.sessionEventOffset = 0;
|
|
46
48
|
uiState.sessionEvents = [];
|
|
49
|
+
// Drop any open spec-evidence preview so a stale A detail cannot survive
|
|
50
|
+
// into node B's spec-evidence panel (AC-007 regression guard).
|
|
51
|
+
uiState.specEvidenceDetail = null;
|
|
52
|
+
uiState.specEvidenceListEvidence = null;
|
|
47
53
|
// Bump the session-event identity token so any in-flight response for the
|
|
48
54
|
// previous node is rejected before it can mutate the current node cache.
|
|
49
55
|
uiState.sessionEventIdentity = makeSessionEventIdentity(dagRunId, nodeId);
|
|
@@ -188,15 +194,20 @@ export function captureDagTimelineViewportState(panel) {
|
|
|
188
194
|
}
|
|
189
195
|
}
|
|
190
196
|
|
|
191
|
-
async function renderSpecEvidence(content, dagRunId, nodeId) {
|
|
192
|
-
const evidence = await fetchJson(
|
|
197
|
+
async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
198
|
+
const evidence = cachedEvidence ?? await fetchJson(
|
|
193
199
|
`/api/dag-runs/${encodeURIComponent(dagRunId)}/nodes/${encodeURIComponent(nodeId)}/spec-evidence`,
|
|
194
200
|
);
|
|
195
201
|
if (!content.isConnected || content.dataset.dagNodeId !== String(nodeId)) return;
|
|
202
|
+
if (uiState.specEvidenceDetail) {
|
|
203
|
+
renderSpecEvidenceDetail(content, dagRunId, nodeId);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
196
206
|
if (!evidence) {
|
|
197
207
|
content.appendChild(el("p", "empty", "无法加载规范证据。"));
|
|
198
208
|
return;
|
|
199
209
|
}
|
|
210
|
+
uiState.specEvidenceListEvidence = evidence;
|
|
200
211
|
|
|
201
212
|
const statusLabels = {
|
|
202
213
|
"spec-read": "已读取规范",
|
|
@@ -241,9 +252,24 @@ async function renderSpecEvidence(content, dagRunId, nodeId) {
|
|
|
241
252
|
});
|
|
242
253
|
appendListSection("已绑定任务源", evidence.sourceBinding?.sources, (source) => {
|
|
243
254
|
const item = el("li", null);
|
|
255
|
+
const button = document.createElement("button");
|
|
256
|
+
button.type = "button";
|
|
257
|
+
button.className = "spec-evidence-file-btn";
|
|
258
|
+
button.setAttribute("data-source", "binding");
|
|
259
|
+
button.setAttribute("data-path", source.path);
|
|
260
|
+
button.id = `spec-evidence-btn-binding-${source.path}`;
|
|
244
261
|
const icon = el("i", "ri-links-line");
|
|
245
262
|
icon.setAttribute("aria-hidden", "true");
|
|
246
|
-
|
|
263
|
+
button.append(
|
|
264
|
+
icon,
|
|
265
|
+
document.createTextNode(` ${source.kind}: `),
|
|
266
|
+
el("code", null, source.path),
|
|
267
|
+
document.createTextNode(` · sha256 ${source.sha256.slice(0, 12)}…`),
|
|
268
|
+
);
|
|
269
|
+
button.addEventListener("click", () =>
|
|
270
|
+
onSpecEvidenceFileClick(content, dagRunId, nodeId, "binding", source.path, button),
|
|
271
|
+
);
|
|
272
|
+
item.appendChild(button);
|
|
247
273
|
return item;
|
|
248
274
|
});
|
|
249
275
|
appendListSection("显式需求编号", evidence.sourceBinding?.requirementIds, (id) => el("li", null, id));
|
|
@@ -252,11 +278,20 @@ async function renderSpecEvidence(content, dagRunId, nodeId) {
|
|
|
252
278
|
evidence.specReads,
|
|
253
279
|
(read) => {
|
|
254
280
|
const item = el("li", null);
|
|
281
|
+
const button = document.createElement("button");
|
|
282
|
+
button.type = "button";
|
|
283
|
+
button.className = "spec-evidence-file-btn";
|
|
284
|
+
button.setAttribute("data-source", "read");
|
|
285
|
+
button.setAttribute("data-path", read.path);
|
|
286
|
+
if (read.timestamp) {
|
|
287
|
+
button.setAttribute("data-read-at", read.timestamp);
|
|
288
|
+
}
|
|
289
|
+
button.id = `spec-evidence-btn-read-${read.path}`;
|
|
255
290
|
const icon = el("i", "ri-file-text-line");
|
|
256
291
|
icon.setAttribute("aria-hidden", "true");
|
|
257
|
-
|
|
292
|
+
button.append(icon, el("code", null, read.path));
|
|
258
293
|
if (read.timestamp) {
|
|
259
|
-
|
|
294
|
+
button.appendChild(
|
|
260
295
|
el(
|
|
261
296
|
"span",
|
|
262
297
|
"spec-evidence-time",
|
|
@@ -264,6 +299,10 @@ async function renderSpecEvidence(content, dagRunId, nodeId) {
|
|
|
264
299
|
),
|
|
265
300
|
);
|
|
266
301
|
}
|
|
302
|
+
button.addEventListener("click", () =>
|
|
303
|
+
onSpecEvidenceFileClick(content, dagRunId, nodeId, "read", read.path, button),
|
|
304
|
+
);
|
|
305
|
+
item.appendChild(button);
|
|
267
306
|
return item;
|
|
268
307
|
},
|
|
269
308
|
);
|
|
@@ -308,7 +347,135 @@ async function renderSpecEvidence(content, dagRunId, nodeId) {
|
|
|
308
347
|
}
|
|
309
348
|
}
|
|
310
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Click handler for spec-evidence file buttons (AC-001/002). Opens the detail
|
|
352
|
+
* view in the same spec-evidence panel (no modal), records the trigger button
|
|
353
|
+
* so the back action can restore keyboard focus (AC-005).
|
|
354
|
+
*/
|
|
355
|
+
function onSpecEvidenceFileClick(content, dagRunId, nodeId, source, relPath, button) {
|
|
356
|
+
openSpecEvidenceDetail(source, relPath, button);
|
|
357
|
+
clearNode(content);
|
|
358
|
+
const detail = uiState.specEvidenceDetail;
|
|
359
|
+
renderSpecEvidenceDetail(content, dagRunId, nodeId);
|
|
360
|
+
void (async () => {
|
|
361
|
+
try {
|
|
362
|
+
const data = await fetchSpecEvidenceFile(dagRunId, nodeId, source, relPath);
|
|
363
|
+
if (
|
|
364
|
+
!content.isConnected ||
|
|
365
|
+
content.dataset.dagNodeId !== String(nodeId) ||
|
|
366
|
+
!uiState.specEvidenceDetail ||
|
|
367
|
+
uiState.specEvidenceDetail.source !== source ||
|
|
368
|
+
uiState.specEvidenceDetail.path !== relPath
|
|
369
|
+
) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
detail.loading = false;
|
|
373
|
+
detail.data = data;
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (
|
|
376
|
+
!content.isConnected ||
|
|
377
|
+
content.dataset.dagNodeId !== String(nodeId) ||
|
|
378
|
+
!uiState.specEvidenceDetail ||
|
|
379
|
+
uiState.specEvidenceDetail.source !== source ||
|
|
380
|
+
uiState.specEvidenceDetail.path !== relPath
|
|
381
|
+
) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
detail.loading = false;
|
|
385
|
+
detail.error = error instanceof Error ? error.message : String(error);
|
|
386
|
+
}
|
|
387
|
+
renderSpecEvidenceDetail(content, dagRunId, nodeId);
|
|
388
|
+
})();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Render the in-inspector file preview (AC-005). Single scroll container for
|
|
393
|
+
* the body, always-visible back button, and accessible loading/error states.
|
|
394
|
+
*/
|
|
395
|
+
function renderSpecEvidenceDetail(content, dagRunId, nodeId) {
|
|
396
|
+
const detail = uiState.specEvidenceDetail;
|
|
397
|
+
clearNode(content);
|
|
398
|
+
if (!detail) {
|
|
399
|
+
renderSpecEvidence(content, dagRunId, nodeId, uiState.specEvidenceListEvidence);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const wrapper = el("div", "spec-evidence-detail");
|
|
404
|
+
const back = document.createElement("button");
|
|
405
|
+
back.type = "button";
|
|
406
|
+
back.className = "spec-evidence-back";
|
|
407
|
+
back.setAttribute("aria-label", "返回规范证据");
|
|
408
|
+
const backIcon = el("i", "ri-arrow-left-line");
|
|
409
|
+
backIcon.setAttribute("aria-hidden", "true");
|
|
410
|
+
back.append(backIcon, document.createTextNode(" 返回规范证据"));
|
|
411
|
+
back.addEventListener("click", () => {
|
|
412
|
+
const closed = closeSpecEvidenceDetail();
|
|
413
|
+
clearNode(content);
|
|
414
|
+
// Restore the list synchronously from the cached evidence so the user
|
|
415
|
+
// sees no flicker and the trigger button is immediately focusable.
|
|
416
|
+
renderSpecEvidence(content, dagRunId, nodeId, uiState.specEvidenceListEvidence);
|
|
417
|
+
if (closed?.triggerId) {
|
|
418
|
+
const trigger = document.getElementById(closed.triggerId);
|
|
419
|
+
trigger?.focus();
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
wrapper.appendChild(back);
|
|
423
|
+
|
|
424
|
+
const meta = el("div", "spec-evidence-detail-meta");
|
|
425
|
+
meta.appendChild(el("code", null, detail.path));
|
|
426
|
+
if (detail.source === "binding" && detail.data) {
|
|
427
|
+
meta.appendChild(
|
|
428
|
+
el(
|
|
429
|
+
"span",
|
|
430
|
+
"spec-evidence-detail-hash",
|
|
431
|
+
`绑定 sha256 ${String(detail.data.sha256 ?? "").slice(0, 12)}… · 当前 ${detail.data.hashMatch ? "一致" : "不一致"}`,
|
|
432
|
+
),
|
|
433
|
+
);
|
|
434
|
+
} else if (detail.source === "read" && detail.data?.readAt) {
|
|
435
|
+
meta.appendChild(
|
|
436
|
+
el(
|
|
437
|
+
"span",
|
|
438
|
+
"spec-evidence-detail-time",
|
|
439
|
+
`读取于 ${formatSessionEventTime({ timestamp: detail.data.readAt })}`,
|
|
440
|
+
),
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
wrapper.appendChild(meta);
|
|
444
|
+
|
|
445
|
+
if (detail.loading) {
|
|
446
|
+
const loading = el("p", "muted", "正在加载文件内容…");
|
|
447
|
+
loading.setAttribute("role", "status");
|
|
448
|
+
loading.setAttribute("aria-live", "polite");
|
|
449
|
+
wrapper.appendChild(loading);
|
|
450
|
+
} else if (detail.error) {
|
|
451
|
+
const error = el("p", "spec-evidence-detail-error", `加载失败:${detail.error}`);
|
|
452
|
+
error.setAttribute("role", "alert");
|
|
453
|
+
wrapper.appendChild(error);
|
|
454
|
+
} else if (detail.data) {
|
|
455
|
+
const body = el("pre", "spec-evidence-detail-content");
|
|
456
|
+
body.textContent = detail.data.content ?? "";
|
|
457
|
+
wrapper.appendChild(body);
|
|
458
|
+
if (detail.data.truncated) {
|
|
459
|
+
const note = el(
|
|
460
|
+
"p",
|
|
461
|
+
"spec-evidence-detail-truncated",
|
|
462
|
+
`已截断,仅显示前 ${detail.data.maxBytes ?? 0} 字节。`,
|
|
463
|
+
);
|
|
464
|
+
wrapper.appendChild(note);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
content.appendChild(wrapper);
|
|
468
|
+
}
|
|
469
|
+
|
|
311
470
|
function fillDagInspectorContent(content, dagRunId, node) {
|
|
471
|
+
const previousRenderedNode = content.dataset.dagNodeId ?? "";
|
|
472
|
+
const nodeChanged = previousRenderedNode !== String(node.nodeId ?? "");
|
|
473
|
+
if (nodeChanged) {
|
|
474
|
+
// Drop any open spec-evidence preview + cached list so a stale A detail
|
|
475
|
+
// cannot survive into node B (AC-007 regression guard).
|
|
476
|
+
uiState.specEvidenceDetail = null;
|
|
477
|
+
uiState.specEvidenceListEvidence = null;
|
|
478
|
+
}
|
|
312
479
|
const activeTab = ["timeline", "spec-evidence"].includes(
|
|
313
480
|
uiState.dagInspectorTab,
|
|
314
481
|
)
|
|
@@ -12,6 +12,7 @@ import { resolveAdapter } from "../../adapters/index.js";
|
|
|
12
12
|
import { loadHarnessManifest } from "../../governance/harness.js";
|
|
13
13
|
import { buildAuthoritySurfaceAuditNode, buildAuthoritySurfaceGateNode, resolveAuthoritySurfaceAudit, } from "./authority-surface.js";
|
|
14
14
|
import { applySddEmbeddedEnhancements, probeRepoLocalSddSkills, } from "./sdd-embedded.js";
|
|
15
|
+
import { discoverProjectGovernancePresence } from "./project-governance-context.js";
|
|
15
16
|
import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
|
|
16
17
|
import { materializeTaskReferenceDocs } from "../../task/source-references.js";
|
|
17
18
|
import { resolveVerifyPreset } from "../../executors/shell-verification.js";
|
|
@@ -1105,6 +1106,7 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
|
1105
1106
|
executorModelMatrix: resolveExecutorModelMatrices(manifest),
|
|
1106
1107
|
verifyCommands,
|
|
1107
1108
|
sddEmbeddedSkills: await probeRepoLocalSddSkills(repoRoot),
|
|
1109
|
+
projectGovernancePresent: await discoverProjectGovernancePresence(repoRoot),
|
|
1108
1110
|
};
|
|
1109
1111
|
return sources;
|
|
1110
1112
|
}
|
|
@@ -4663,11 +4665,35 @@ function buildHybridDagForTemplate(sources, template) {
|
|
|
4663
4665
|
else
|
|
4664
4666
|
spec = buildSupervisedHybridDag(standard, sources);
|
|
4665
4667
|
}
|
|
4668
|
+
applyProjectGovernanceReview(spec, template, sources);
|
|
4666
4669
|
spec.sourceBinding = buildDagSourceBinding(sources);
|
|
4670
|
+
assertNoGovernanceFlagOnDisallowedTemplate(spec, template);
|
|
4667
4671
|
parseDagSpec(spec);
|
|
4668
4672
|
assertValidDagSpec(spec);
|
|
4669
4673
|
return spec;
|
|
4670
4674
|
}
|
|
4675
|
+
const GOVERNANCE_DISALLOWED_TEMPLATES = new Set([
|
|
4676
|
+
"frontend-implementation",
|
|
4677
|
+
"frontend-test-dag",
|
|
4678
|
+
"backend-test-dag",
|
|
4679
|
+
"knowledge-sync-dag",
|
|
4680
|
+
"knowledge-graph-bootstrap-dag",
|
|
4681
|
+
]);
|
|
4682
|
+
/**
|
|
4683
|
+
* Generate-time guard (constraint 2 / AC-007): the governance standard review
|
|
4684
|
+
* flag must never appear on knowledge-sync, knowledge-graph-bootstrap,
|
|
4685
|
+
* backend-test, or frontend tasks. Only standard verify-pi and the shared
|
|
4686
|
+
* review-pi (reviewed/supervised) opt in explicitly.
|
|
4687
|
+
*/
|
|
4688
|
+
function assertNoGovernanceFlagOnDisallowedTemplate(spec, template) {
|
|
4689
|
+
if (!GOVERNANCE_DISALLOWED_TEMPLATES.has(template))
|
|
4690
|
+
return;
|
|
4691
|
+
for (const task of spec.tasks) {
|
|
4692
|
+
if (task.governanceStandardReview) {
|
|
4693
|
+
throw new Error(`governanceStandardReview must not be set on ${template} task ${task.id}`);
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
}
|
|
4671
4697
|
export function buildHybridDagFromTask(sources, options = {}) {
|
|
4672
4698
|
const selection = resolveTaskDagTemplateSelection({
|
|
4673
4699
|
taskKind: sources.taskConfig.taskKind,
|
|
@@ -4791,6 +4817,84 @@ function buildReviewGateNode(sources) {
|
|
|
4791
4817
|
},
|
|
4792
4818
|
};
|
|
4793
4819
|
}
|
|
4820
|
+
function enableProjectGovernanceOnNode(task) {
|
|
4821
|
+
task.governanceStandardReview = true;
|
|
4822
|
+
}
|
|
4823
|
+
/**
|
|
4824
|
+
* Apply governance only to general implementation DAGs, and only when the
|
|
4825
|
+
* target repository actually contains AGENTS.md. Standard reuses verify-pi;
|
|
4826
|
+
* reviewed/supervised reuse review-pi and their existing review verdict gate.
|
|
4827
|
+
*/
|
|
4828
|
+
function applyProjectGovernanceReview(spec, template, sources) {
|
|
4829
|
+
if (!sources.projectGovernancePresent)
|
|
4830
|
+
return;
|
|
4831
|
+
if (template === "standard-dag") {
|
|
4832
|
+
const verify = spec.tasks.find((task) => task.id === "verify-pi");
|
|
4833
|
+
if (!verify)
|
|
4834
|
+
return;
|
|
4835
|
+
enableProjectGovernanceOnNode(verify);
|
|
4836
|
+
insertGovernanceStandardGate(spec, sources);
|
|
4837
|
+
return;
|
|
4838
|
+
}
|
|
4839
|
+
if (template === "review-gated-dag" || template === "supervised-implementation") {
|
|
4840
|
+
const review = spec.tasks.find((task) => task.id === "review-pi");
|
|
4841
|
+
if (review)
|
|
4842
|
+
enableProjectGovernanceOnNode(review);
|
|
4843
|
+
}
|
|
4844
|
+
}
|
|
4845
|
+
/**
|
|
4846
|
+
* Deterministic governance standard gate for standard-dag closeout. It reuses
|
|
4847
|
+
* the standard `verdictGate` preset (fromNodeId=verify-pi, accept
|
|
4848
|
+
* ["VERDICT: pass"], first-verdict-line). When no applicable AGENTS.md/standard
|
|
4849
|
+
* exists, projectGovernanceGate returns success without parsing a verdict.
|
|
4850
|
+
* When an applicable mandatory standard is violated, verify-pi must emit
|
|
4851
|
+
* VERDICT: request-revision and the gate blocks closeout. No new model node.
|
|
4852
|
+
*/
|
|
4853
|
+
function buildGovernanceStandardGateNode(sources) {
|
|
4854
|
+
return {
|
|
4855
|
+
id: "governance-standard-gate-shell",
|
|
4856
|
+
depends_on: ["verify-pi"],
|
|
4857
|
+
role: "verifier",
|
|
4858
|
+
executor: "shell",
|
|
4859
|
+
complexity: "LOW",
|
|
4860
|
+
writePolicy: "read-only",
|
|
4861
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
4862
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
4863
|
+
outputContract: "Deterministic governance verdict gate: exit 0 only when verify-pi first VERDICT line is pass (covers applicable mandatory standard compliance). No file writes.",
|
|
4864
|
+
subtask_prompt: "Deterministic gate: block closeout unless verify-pi emitted VERDICT: pass, which requires no unresolved applicable mandatory governance-standard violation. Read-only: do not modify files.",
|
|
4865
|
+
shell: {
|
|
4866
|
+
commands: [],
|
|
4867
|
+
projectGovernanceGate: {
|
|
4868
|
+
contextPath: ".runtime/project-governance-context.json",
|
|
4869
|
+
},
|
|
4870
|
+
verdictGate: {
|
|
4871
|
+
fromNodeId: "verify-pi",
|
|
4872
|
+
accept: ["VERDICT: pass"],
|
|
4873
|
+
label: "governance-standard",
|
|
4874
|
+
lineMode: "first-verdict-line",
|
|
4875
|
+
},
|
|
4876
|
+
cwd: ".",
|
|
4877
|
+
timeoutMs: 60000,
|
|
4878
|
+
},
|
|
4879
|
+
};
|
|
4880
|
+
}
|
|
4881
|
+
/**
|
|
4882
|
+
* Insert the governance-standard gate between verify-pi and closeout-pi in a
|
|
4883
|
+
* standard DAG. Idempotent and no-op if verify-pi/closeout-pi are absent.
|
|
4884
|
+
*/
|
|
4885
|
+
function insertGovernanceStandardGate(spec, sources) {
|
|
4886
|
+
const closeout = spec.tasks.find((task) => task.id === "closeout-pi");
|
|
4887
|
+
const verify = spec.tasks.find((task) => task.id === "verify-pi");
|
|
4888
|
+
if (!closeout || !verify)
|
|
4889
|
+
return;
|
|
4890
|
+
if (spec.tasks.some((task) => task.id === "governance-standard-gate-shell")) {
|
|
4891
|
+
return;
|
|
4892
|
+
}
|
|
4893
|
+
const gate = buildGovernanceStandardGateNode(sources);
|
|
4894
|
+
const closeoutIndex = spec.tasks.findIndex((task) => task.id === "closeout-pi");
|
|
4895
|
+
spec.tasks.splice(closeoutIndex, 0, gate);
|
|
4896
|
+
closeout.depends_on = ["governance-standard-gate-shell"];
|
|
4897
|
+
}
|
|
4794
4898
|
function buildReviewGatedHybridDag(standard, sources) {
|
|
4795
4899
|
const spec = {
|
|
4796
4900
|
...standard,
|
|
@@ -9,11 +9,12 @@ import { buildDagNodePromptEnvelope } from "./prompt.js";
|
|
|
9
9
|
import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
|
|
10
10
|
import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, } from "./retry-policy.js";
|
|
11
11
|
import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
12
|
+
import { buildProjectGovernanceContext, readCompletedWriterChangeManifests, writeProjectGovernanceContext, } from "./project-governance-context.js";
|
|
12
13
|
import { assertSkillSnapshotCoversSpec, buildNodePromptFromSnapshot, isDagSkillSnapshotIntegrityError, readSkillSnapshot, } from "./skill-snapshot.js";
|
|
13
14
|
import { resolveDagSkillInstructions, skillInstructionMetadata, } from "./skill-instructions.js";
|
|
14
15
|
import { parseRepairArtifactFromText, resolveRepairTaskForGate, validateRepairArtifactScope, } from "./repair-artifact.js";
|
|
15
16
|
import { resolveModelForTask, } from "./types.js";
|
|
16
|
-
export function buildNodePrompt(spec, task, upstream) {
|
|
17
|
+
export function buildNodePrompt(spec, task, upstream, options) {
|
|
17
18
|
const policy = resolveContextPolicy(spec);
|
|
18
19
|
return buildDagNodePromptEnvelope({
|
|
19
20
|
spec,
|
|
@@ -21,9 +22,10 @@ export function buildNodePrompt(spec, task, upstream) {
|
|
|
21
22
|
upstream,
|
|
22
23
|
resolvedSkills: policy.resolveSkills(spec, task),
|
|
23
24
|
maxUpstreamChars: policy.resolveMaxUpstreamChars(task),
|
|
25
|
+
projectGovernanceContext: options?.projectGovernanceContext,
|
|
24
26
|
});
|
|
25
27
|
}
|
|
26
|
-
export async function buildNodePromptWithResolvedSkillInstructions(spec, task, upstream, cwd) {
|
|
28
|
+
export async function buildNodePromptWithResolvedSkillInstructions(spec, task, upstream, cwd, options) {
|
|
27
29
|
const policy = resolveContextPolicy(spec);
|
|
28
30
|
const skillNames = policy.resolveSkills(spec, task);
|
|
29
31
|
const budget = policy.resolveSkillInstructionBudget(task);
|
|
@@ -43,6 +45,7 @@ export async function buildNodePromptWithResolvedSkillInstructions(spec, task, u
|
|
|
43
45
|
resolvedSkills: skillNames,
|
|
44
46
|
resolvedSkillInstructions,
|
|
45
47
|
maxUpstreamChars: policy.resolveMaxUpstreamChars(task),
|
|
48
|
+
projectGovernanceContext: options?.projectGovernanceContext,
|
|
46
49
|
}),
|
|
47
50
|
resolvedSkills: skillInstructionMetadata(resolvedSkillInstructions),
|
|
48
51
|
};
|
|
@@ -138,12 +141,12 @@ export async function executeDagNode(input) {
|
|
|
138
141
|
const { nodeId, tasksById, state, spec, cwd, runDir, executeNode } = input;
|
|
139
142
|
const task = tasksById.get(nodeId);
|
|
140
143
|
const node = state.nodes[nodeId];
|
|
141
|
-
const
|
|
144
|
+
const failBeforePrompt = async (error, failureCategory) => {
|
|
142
145
|
const failedAt = new Date().toISOString();
|
|
143
146
|
node.startedAt ??= failedAt;
|
|
144
147
|
node.status = "ERROR";
|
|
145
148
|
node.stderr = error instanceof Error ? error.message : String(error);
|
|
146
|
-
node.failureCategory =
|
|
149
|
+
node.failureCategory = failureCategory;
|
|
147
150
|
node.finishedAt = failedAt;
|
|
148
151
|
node.lastActivityAt = node.finishedAt;
|
|
149
152
|
node.durationMs = Math.max(0, Date.now() - new Date(node.startedAt).getTime());
|
|
@@ -152,6 +155,29 @@ export async function executeDagNode(input) {
|
|
|
152
155
|
await input.persistState();
|
|
153
156
|
await notifyNodeObserver(input.observer, "onNodeFinish", nodeId, state);
|
|
154
157
|
};
|
|
158
|
+
const failSkillSnapshot = (error) => failBeforePrompt(error, "skill-snapshot-integrity");
|
|
159
|
+
let projectGovernanceContext;
|
|
160
|
+
if (task.governanceStandardReview) {
|
|
161
|
+
try {
|
|
162
|
+
const changeManifest = await readCompletedWriterChangeManifests({
|
|
163
|
+
runDir,
|
|
164
|
+
spec,
|
|
165
|
+
state,
|
|
166
|
+
});
|
|
167
|
+
projectGovernanceContext = await buildProjectGovernanceContext({
|
|
168
|
+
runId: state.runId,
|
|
169
|
+
cwd,
|
|
170
|
+
changeManifest,
|
|
171
|
+
runDir,
|
|
172
|
+
});
|
|
173
|
+
state.projectGovernanceContextRef = await writeProjectGovernanceContext(runDir, projectGovernanceContext);
|
|
174
|
+
await input.persistState();
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
await failBeforePrompt(error, "project-governance-integrity");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
155
181
|
const isDynamicTask = Boolean(task.dynamicExpansion
|
|
156
182
|
|| task.dynamicReduction
|
|
157
183
|
|| task.dynamicCondition
|
|
@@ -169,6 +195,7 @@ export async function executeDagNode(input) {
|
|
|
169
195
|
task,
|
|
170
196
|
upstream: state.nodes,
|
|
171
197
|
snapshot: skillSnapshot,
|
|
198
|
+
projectGovernanceContext,
|
|
172
199
|
});
|
|
173
200
|
}
|
|
174
201
|
}
|
|
@@ -237,7 +264,7 @@ export async function executeDagNode(input) {
|
|
|
237
264
|
}
|
|
238
265
|
else {
|
|
239
266
|
({ prompt, resolvedSkills } =
|
|
240
|
-
await buildNodePromptWithResolvedSkillInstructions(spec, task, state.nodes, cwd));
|
|
267
|
+
await buildNodePromptWithResolvedSkillInstructions(spec, task, state.nodes, cwd, { projectGovernanceContext }));
|
|
241
268
|
}
|
|
242
269
|
}
|
|
243
270
|
catch (error) {
|