@tea-agent/loop-agent 0.33.5 → 0.33.6

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/application/task-lifecycle/advance.js +254 -4
  3. package/dist/application/task-lifecycle/gates.js +50 -0
  4. package/dist/application/task-lifecycle/observe.js +11 -2
  5. package/dist/commands/init-upgrade.js +32 -1
  6. package/dist/commands/init.js +94 -3
  7. package/dist/executors/shell-write-guard.js +26 -8
  8. package/dist/shared/operator/capabilities.js +72 -42
  9. package/dist/shared/resilient-git.js +133 -0
  10. package/dist/task/source-prepare/artifact-meta.js +137 -0
  11. package/dist/task/source-prepare/index.js +2 -0
  12. package/dist/task/source-prepare/parse-intent.js +58 -10
  13. package/dist/task/source-prepare/prepare.js +180 -16
  14. package/dist/task/source-prepare/reference-integrity.js +18 -2
  15. package/dist/task/source-prepare/semantic-intake.js +404 -0
  16. package/dist/worker/console/app-data.js +2 -0
  17. package/dist/worker/console/chat/chat-event-store.js +190 -25
  18. package/dist/worker/console/chat/pi-console-config.js +250 -32
  19. package/dist/worker/console/chat/pi-runtime.js +625 -71
  20. package/dist/worker/console/chat/resource-loader.js +5 -4
  21. package/dist/worker/console/chat/routes.js +324 -146
  22. package/dist/worker/console/chat/runtime-context.js +48 -12
  23. package/dist/worker/console/chat/runtime-selection.js +59 -0
  24. package/dist/worker/console/chat/shortcuts.js +1 -0
  25. package/dist/worker/console/chat/tool-adapter.js +9 -3
  26. package/dist/worker/console/chat/tools.js +5 -1
  27. package/dist/worker/console/dag-execution-receipt.js +380 -0
  28. package/dist/worker/console/operator-actions.js +559 -68
  29. package/dist/worker/console/server.js +8 -15
  30. package/dist/worker/console/static/assets/index-BUOLppPr.js +28 -0
  31. package/dist/worker/console/static/assets/index-C1KzazY5.css +1 -0
  32. package/dist/worker/console/static/index.html +2 -2
  33. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
  34. package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
  35. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
  36. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +16 -0
  37. package/dist/worker/console/static-src/operator-chat/useChatStream.js +210 -184
  38. package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
  39. package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
  40. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
  41. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
  42. package/dist/worker/delivery/final-verification.js +13 -5
  43. package/dist/worker/delivery/package.js +31 -19
  44. package/dist/worker/delivery/verification-bundle.js +6 -4
  45. package/dist/worker/observe/static/operator-chrome.css +5 -2
  46. package/dist/worker/observe/static/operator-chrome.js +6 -1
  47. package/dist/worker/observe/static/styles.css +39 -9
  48. package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
  49. package/dist/workflows/dag/workspace-checkpoint.js +8 -27
  50. package/harness.json +1 -1
  51. package/package.json +1 -1
  52. package/skills/loop-agent/references/command-reference.md +3 -1
  53. package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
  54. package/skills/loop-agent/references/task-workflow.md +4 -0
  55. package/dist/worker/console/chat/instruction-skills.js +0 -217
  56. package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
  57. package/dist/worker/console/static/assets/index-CteJFFL2.js +0 -29
@@ -1,8 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { execFile } from "node:child_process";
3
2
  import { mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
4
3
  import path from "node:path";
5
- import { promisify } from "node:util";
4
+ import { isResilientGitCommandError, runResilientGitCommand } from "../../shared/resilient-git.js";
6
5
  import YAML from "yaml";
7
6
  import { z } from "zod";
8
7
  import { controllerIdentitiesMatch, LOOP_AGENT_PACKAGE_NAME, } from "../loop-agent/loop-agent-client.js";
@@ -13,7 +12,6 @@ import { validateFeatureTaskGraph } from "../task-graph/validate.js";
13
12
  import { taskSpecSchema } from "../task-spec/schema.js";
14
13
  import { gitTransactionRecordSchema, transactionRecordPath } from "./git-transaction.js";
15
14
  import { readVerifiedBundle, verifyBundleCurrentStateBindings, verifyBundleOutcomes, verifyBundleTaskSpecBindings } from "./verification-bundle.js";
16
- const execFileAsync = promisify(execFile);
17
15
  const hashedRefSchema = z.object({ path: z.string().min(1), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict();
18
16
  export const acceptanceCoverageArtifactSchema = z.object({
19
17
  schemaVersion: z.literal(1),
@@ -91,9 +89,9 @@ export async function prepareFeatureDelivery(input) {
91
89
  const now = input.now ?? new Date();
92
90
  const record = await readTransaction(repoRoot, featureId, blockers);
93
91
  if (record) {
94
- const branch = await git(repoRoot, ["branch", "--show-current"]).catch(() => "");
95
- const head = await git(repoRoot, ["rev-parse", "HEAD"]).catch(() => "");
96
- const status = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]).catch(() => "git-error");
92
+ const branch = await git(repoRoot, ["branch", "--show-current"]).catch(rethrowInfrastructureOr(""));
93
+ const head = await git(repoRoot, ["rev-parse", "HEAD"]).catch(rethrowInfrastructureOr(""));
94
+ const status = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]).catch(rethrowInfrastructureOr("git-error"));
97
95
  if (branch !== record.branch)
98
96
  blockers.push(`current branch must be ${record.branch}`);
99
97
  if (head !== record.lastCheckpoint)
@@ -202,11 +200,11 @@ export async function validateDeliveryPackage(input) {
202
200
  blockers.push("Delivery Feature ownership mismatch");
203
201
  if (manifest.branch !== record.branch || manifest.baseBranch !== record.baseBranch || manifest.baseSha !== record.baseCommit || manifest.headSha !== record.lastCheckpoint)
204
202
  blockers.push("Delivery manifest does not match the Git transaction");
205
- const branch = await git(repoRoot, ["branch", "--show-current"]).catch(() => "");
206
- const head = await git(repoRoot, ["rev-parse", "HEAD"]).catch(() => "");
203
+ const branch = await git(repoRoot, ["branch", "--show-current"]).catch(rethrowInfrastructureOr(""));
204
+ const head = await git(repoRoot, ["rev-parse", "HEAD"]).catch(rethrowInfrastructureOr(""));
207
205
  if (branch !== record.branch || head !== record.lastCheckpoint)
208
206
  blockers.push("current Git position does not match Delivery");
209
- const rawCloseoutStatus = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]).catch(() => "git-error");
207
+ const rawCloseoutStatus = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]).catch(rethrowInfrastructureOr("git-error"));
210
208
  const allowedDirtyPaths = new Set((input.allowedDirtyPaths ?? []).map((item) => item.replace(/\\/g, "/")));
211
209
  const closeoutStatus = rawCloseoutStatus.split("\n").filter(Boolean).filter((line) => !allowedDirtyPaths.has(line.slice(3).replace(/\\/g, "/"))).join("\n").trim();
212
210
  if (closeoutStatus)
@@ -495,23 +493,37 @@ async function writeDeliveryArtifacts(artifacts, coverage, manifest) {
495
493
  }
496
494
  }
497
495
  function renderPr(manifest) { return `# Summary\n\nFeature ${manifest.featureId} local Delivery Package.\n\n## Feature/base/head\n\n- Branch: ${manifest.branch}\n- Base: ${manifest.baseBranch} @ ${manifest.baseSha}\n- Head: ${manifest.headSha}\n\n## Completed Tasks\n\n${manifest.tasks.map((task) => `- ${task.taskId}: ${task.commit} (${task.changedFiles.join(", ")})`).join("\n")}\n\n## Acceptance Coverage\n\n- Required: ${manifest.acceptanceCoverage.requiredTotal}\n- Covered: ${manifest.acceptanceCoverage.covered}\n- Waived: ${manifest.acceptanceCoverage.waived}\n- Blocked: ${manifest.acceptanceCoverage.blocked}\n\n## Verification\n\n- QA: ${manifest.qa.evidence.path}\n- Final: ${manifest.finalVerification.path}\n\n## Changed Files\n\n${[...new Set(manifest.tasks.flatMap((task) => task.changedFiles))].map((file) => `- ${file}`).join("\n")}\n\n## Risks\n\n- high=${manifest.riskSummary.high}, medium=${manifest.riskSummary.medium}, low=${manifest.riskSummary.low}\n\n## Human Review Checklist\n\n- [ ] Review diff and commits\n- [ ] Confirm required AC coverage and waivers\n- [ ] Confirm QA and final verification evidence\n- [ ] Review risks\n- [ ] Decide whether to push or create a remote PR manually\n`; }
498
- async function git(repoRoot, args) { const result = await execFileAsync("git", args, { cwd: repoRoot, encoding: "utf8" }); return result.stdout.trim(); }
499
- async function gitRaw(repoRoot, args) { const result = await execFileAsync("git", args, { cwd: repoRoot, encoding: "utf8" }); return result.stdout; }
500
- async function gitOk(repoRoot, args) { try {
501
- await git(repoRoot, args);
502
- return true;
496
+ async function git(repoRoot, args) {
497
+ const result = await runResilientGitCommand({ cwd: repoRoot, args, readOnly: true });
498
+ if (result.code !== 0)
499
+ throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
500
+ return result.stdout.trim();
501
+ }
502
+ async function gitRaw(repoRoot, args) {
503
+ const result = await runResilientGitCommand({ cwd: repoRoot, args, readOnly: true });
504
+ if (result.code !== 0)
505
+ throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
506
+ return result.stdout;
507
+ }
508
+ async function gitOk(repoRoot, args) {
509
+ const result = await runResilientGitCommand({ cwd: repoRoot, args, readOnly: true });
510
+ return result.code === 0;
511
+ }
512
+ function rethrowInfrastructureOr(fallback) {
513
+ return (error) => {
514
+ if (isResilientGitCommandError(error))
515
+ throw error;
516
+ return fallback;
517
+ };
503
518
  }
504
- catch {
505
- return false;
506
- } }
507
519
  async function checkpointMatches(repoRoot, head, checkpoint, spec) {
508
520
  if (!await gitOk(repoRoot, ["merge-base", "--is-ancestor", checkpoint.commit, head]))
509
521
  return false;
510
- const messageText = await gitRaw(repoRoot, ["show", "-s", "--format=%B", checkpoint.commit]).catch(() => "");
522
+ const messageText = await gitRaw(repoRoot, ["show", "-s", "--format=%B", checkpoint.commit]).catch(rethrowInfrastructureOr(""));
511
523
  for (const trailer of [`Feature: ${spec.feature_id}`, `Task: ${spec.id}`, `Acceptance: ${spec.acceptance_refs.join(", ")}`, `Worker-Run: ${checkpoint.workerRunId}`])
512
524
  if (!messageText.split(/\r?\n/).includes(trailer))
513
525
  return false;
514
- const actual = (await gitRaw(repoRoot, ["diff-tree", "--no-commit-id", "--name-only", "-r", checkpoint.commit]).catch(() => "")).split(/\r?\n/).filter(Boolean).sort();
526
+ const actual = (await gitRaw(repoRoot, ["diff-tree", "--no-commit-id", "--name-only", "-r", checkpoint.commit]).catch(rethrowInfrastructureOr(""))).split(/\r?\n/).filter(Boolean).sort();
515
527
  return JSON.stringify(actual) === JSON.stringify([...checkpoint.changedFiles].sort());
516
528
  }
517
529
  async function hashedRefValid(repoRoot, ref) { try {
@@ -7,6 +7,7 @@ import { artifactMatchesEvidenceToken, isShellVerificationToken, } from "../outc
7
7
  import { readVerifiedOutcome } from "../outcomes/store.js";
8
8
  import { getTaskPoolRoot, readFeatureTaskPoolStates } from "../pool/run-store.js";
9
9
  import { workflowSchema } from "../task-spec/workflow-routing.js";
10
+ import { runResilientGitCommand } from "../../shared/resilient-git.js";
10
11
  import { gitTransactionRecordSchema, transactionRecordPath } from "./git-transaction.js";
11
12
  /**
12
13
  * Feature Verification Bundle v1 — a hash-bound aggregate of completed typed
@@ -533,10 +534,11 @@ async function readGitRecord(repoRoot, featureId) {
533
534
  }
534
535
  }
535
536
  async function defaultGit(repoRoot, args) {
536
- const { execFile } = await import("node:child_process");
537
- const { promisify } = await import("node:util");
538
- const execFileAsync = promisify(execFile);
539
- return (await execFileAsync("git", args, { cwd: repoRoot, encoding: "utf8" })).stdout.trim();
537
+ const result = await runResilientGitCommand({ cwd: repoRoot, args, readOnly: true });
538
+ if (result.code !== 0) {
539
+ throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
540
+ }
541
+ return result.stdout.trim();
540
542
  }
541
543
  // Re-export the workflow schema so consumers can validate typed workflows.
542
544
  export { workflowSchema };
@@ -71,8 +71,11 @@
71
71
  text-decoration: none;
72
72
  white-space: nowrap;
73
73
  }
74
- .och-brand svg {
75
- color: var(--och-orange);
74
+ .och-brand-mark {
75
+ display: block;
76
+ width: 32px;
77
+ height: 32px;
78
+ object-fit: contain;
76
79
  flex-shrink: 0;
77
80
  }
78
81
  .och-brand:hover {
@@ -353,7 +353,12 @@ export function mountOperatorChrome(root, config = {}) {
353
353
 
354
354
  const brand = el("a", "och-brand");
355
355
  brand.href = model.homeHref;
356
- icon(brand, "radar");
356
+ brand.setAttribute("aria-label", `${model.brand} 首页`);
357
+ const brandMark = el("img", "och-brand-mark");
358
+ brandMark.src = "/inspect/favicon.svg";
359
+ brandMark.alt = "";
360
+ brandMark.setAttribute("aria-hidden", "true");
361
+ brand.appendChild(brandMark);
357
362
  brand.appendChild(el("span", null, model.brand));
358
363
  global.appendChild(brand);
359
364
 
@@ -1617,7 +1617,11 @@ body.is-resizing-dag-graph {
1617
1617
  /* 历史失败已恢复/已覆盖:独立中性样式(不占用当前终态失败红色,不洗白事实)。 */
1618
1618
  .dag-trajectory-occurrence.is-recovered {
1619
1619
  border-color: color-mix(in srgb, var(--amber, #b58900) 55%, var(--hairline));
1620
- background: color-mix(in srgb, var(--amber-soft, #fff7d6) 55%, var(--surface));
1620
+ background: color-mix(
1621
+ in srgb,
1622
+ var(--amber-soft, #fff7d6) 55%,
1623
+ var(--surface)
1624
+ );
1621
1625
  }
1622
1626
  .dag-trajectory-recovered {
1623
1627
  font-size: 11px;
@@ -1647,9 +1651,14 @@ body.is-resizing-dag-graph {
1647
1651
  }
1648
1652
  .dag-trajectory-banner {
1649
1653
  padding: 8px 12px;
1650
- border: 1px solid color-mix(in srgb, var(--amber, #b58900) 50%, var(--hairline));
1654
+ border: 1px solid
1655
+ color-mix(in srgb, var(--amber, #b58900) 50%, var(--hairline));
1651
1656
  border-radius: var(--radius-lg);
1652
- background: color-mix(in srgb, var(--amber-soft, #fff7d6) 60%, var(--surface));
1657
+ background: color-mix(
1658
+ in srgb,
1659
+ var(--amber-soft, #fff7d6) 60%,
1660
+ var(--surface)
1661
+ );
1653
1662
  color: var(--body);
1654
1663
  font-size: 12px;
1655
1664
  }
@@ -3050,7 +3059,11 @@ button.copy-command:focus-visible {
3050
3059
  }
3051
3060
  .dags-copy-btn:active {
3052
3061
  border-color: var(--hairline-strong, #cfcdc4);
3053
- background: color-mix(in srgb, var(--surface-muted, #f0efe9) 80%, var(--hairline, #e6e5e0));
3062
+ background: color-mix(
3063
+ in srgb,
3064
+ var(--surface-muted, #f0efe9) 80%,
3065
+ var(--hairline, #e6e5e0)
3066
+ );
3054
3067
  }
3055
3068
  /* Mouse focus must not leave residual orange chrome; orange is :focus-visible only. */
3056
3069
  .dags-copy-btn:focus:not(:focus-visible) {
@@ -3201,12 +3214,20 @@ button.copy-command:focus-visible {
3201
3214
  }
3202
3215
  .dags-page-num:hover:not(:disabled),
3203
3216
  .dags-page-nav:hover:not(:disabled) {
3204
- background: color-mix(in srgb, var(--orange-soft, #fff1ea) 70%, var(--surface-muted, #f0efe9));
3217
+ background: color-mix(
3218
+ in srgb,
3219
+ var(--orange-soft, #fff1ea) 70%,
3220
+ var(--surface-muted, #f0efe9)
3221
+ );
3205
3222
  color: var(--ink, #26251e);
3206
3223
  }
3207
3224
  .dags-page-num:active:not(:disabled),
3208
3225
  .dags-page-nav:active:not(:disabled) {
3209
- background: color-mix(in srgb, var(--orange, #f54e00) 14%, var(--surface-muted, #f0efe9));
3226
+ background: color-mix(
3227
+ in srgb,
3228
+ var(--orange, #f54e00) 14%,
3229
+ var(--surface-muted, #f0efe9)
3230
+ );
3210
3231
  }
3211
3232
  .dags-page-num:focus-visible,
3212
3233
  .dags-page-nav:focus-visible,
@@ -3224,7 +3245,8 @@ button.copy-command:focus-visible {
3224
3245
  border-color: var(--orange, #f54e00);
3225
3246
  color: #fff;
3226
3247
  opacity: 1;
3227
- box-shadow: 0 2px 8px color-mix(in srgb, var(--orange, #f54e00) 35%, transparent);
3248
+ box-shadow: 0 2px 8px
3249
+ color-mix(in srgb, var(--orange, #f54e00) 35%, transparent);
3228
3250
  cursor: default;
3229
3251
  }
3230
3252
  .dags-page-nav:disabled,
@@ -3320,7 +3342,11 @@ button.copy-command:focus-visible {
3320
3342
  background: var(--surface-muted, #f0efe9);
3321
3343
  }
3322
3344
  .dags-refresh-btn:active:not(:disabled) {
3323
- background: color-mix(in srgb, var(--surface-muted, #f0efe9) 80%, var(--hairline, #e6e5e0));
3345
+ background: color-mix(
3346
+ in srgb,
3347
+ var(--surface-muted, #f0efe9) 80%,
3348
+ var(--hairline, #e6e5e0)
3349
+ );
3324
3350
  }
3325
3351
  .dags-pagination.is-busy {
3326
3352
  opacity: 0.92;
@@ -3516,7 +3542,11 @@ button.copy-command:focus-visible {
3516
3542
  }
3517
3543
  .custom-select-option.is-selected.is-active,
3518
3544
  .custom-select-option.is-selected:hover {
3519
- background: color-mix(in srgb, var(--orange-soft, #fff1ea) 70%, var(--surface-muted, #f0efe9));
3545
+ background: color-mix(
3546
+ in srgb,
3547
+ var(--orange-soft, #fff1ea) 70%,
3548
+ var(--surface-muted, #f0efe9)
3549
+ );
3520
3550
  }
3521
3551
  .custom-select-option-label {
3522
3552
  flex: 1 1 auto;
@@ -1,31 +1,14 @@
1
1
  import { createHash } from "node:crypto";
2
- import { spawn } from "node:child_process";
3
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
4
3
  import path from "node:path";
4
+ import { runResilientGitCommand } from "../../shared/resilient-git.js";
5
5
  import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
6
6
  import { pathMatchesPattern } from "../../shared/git-progress.js";
7
7
  export const FRONTEND_WORKTREE_DIFF_SCHEMA_ID = "frontend-worktree-diff-v1";
8
8
  export const FRONTEND_WORKTREE_BASELINE_SCHEMA_ID = "frontend-worktree-baseline-v1";
9
- function runGit(cwd, args) {
10
- return new Promise((resolve, reject) => {
11
- const child = spawn("git", args, {
12
- cwd,
13
- env: process.env,
14
- stdio: ["ignore", "pipe", "pipe"],
15
- });
16
- let stdout = "";
17
- let stderr = "";
18
- child.stdout.on("data", (chunk) => {
19
- stdout += String(chunk);
20
- });
21
- child.stderr.on("data", (chunk) => {
22
- stderr += String(chunk);
23
- });
24
- child.on("error", reject);
25
- child.on("close", (code) => {
26
- resolve({ code: code ?? 1, stdout, stderr });
27
- });
28
- });
9
+ async function runGit(cwd, args) {
10
+ const result = await runResilientGitCommand({ cwd, args, readOnly: true });
11
+ return { code: result.code, stdout: result.stdout, stderr: result.stderr };
29
12
  }
30
13
  function splitLines(text) {
31
14
  return text
@@ -52,8 +35,9 @@ async function sha256File(filePath) {
52
35
  }
53
36
  }
54
37
  export async function captureFrontendWorktreeBaseline(input) {
38
+ const run = input.dependencies?.runGit ?? runGit;
55
39
  const root = path.resolve(input.workspaceRoot);
56
- const status = await runGit(root, [
40
+ const status = await run(root, [
57
41
  "status",
58
42
  "--porcelain=v1",
59
43
  "--untracked-files=all",
@@ -84,6 +68,7 @@ export async function captureFrontendWorktreeBaseline(input) {
84
68
  * patch + inventory under the current run directory before review runs.
85
69
  */
86
70
  export async function runFrontendWorktreeDiffGate(input) {
71
+ const run = input.dependencies?.runGit ?? runGit;
87
72
  const root = path.resolve(input.workspaceRoot);
88
73
  let baseline;
89
74
  try {
@@ -101,16 +86,16 @@ export async function runFrontendWorktreeDiffGate(input) {
101
86
  if (input.requireBaseline && !baseline) {
102
87
  throw new Error("frontend worktree baseline is required for this review context");
103
88
  }
104
- const revParse = await runGit(root, ["rev-parse", "--is-inside-work-tree"]);
89
+ const revParse = await run(root, ["rev-parse", "--is-inside-work-tree"]);
105
90
  if (revParse.code !== 0 || revParse.stdout.trim() !== "true") {
106
91
  throw new Error(`frontend worktree diff gate requires a git worktree: ${revParse.stderr.trim() || revParse.stdout.trim() || "not a git repository"}`);
107
92
  }
108
93
  // Match failure capture semantics: binary-capable tracked diff vs HEAD.
109
- const diff = await runGit(root, ["diff", "--binary", "HEAD"]);
94
+ const diff = await run(root, ["diff", "--binary", "HEAD"]);
110
95
  if (diff.code !== 0) {
111
96
  throw new Error(`frontend worktree diff gate git diff failed: ${diff.stderr.trim() || diff.stdout.trim()}`);
112
97
  }
113
- const nameOnly = await runGit(root, [
98
+ const nameOnly = await run(root, [
114
99
  "diff",
115
100
  "--name-only",
116
101
  "--diff-filter=ACDMRTUXB",
@@ -119,7 +104,7 @@ export async function runFrontendWorktreeDiffGate(input) {
119
104
  if (nameOnly.code !== 0) {
120
105
  throw new Error(`frontend worktree diff gate git name-only failed: ${nameOnly.stderr.trim() || nameOnly.stdout.trim()}`);
121
106
  }
122
- const untracked = await runGit(root, [
107
+ const untracked = await run(root, [
123
108
  "ls-files",
124
109
  "--others",
125
110
  "--exclude-standard",
@@ -152,7 +137,7 @@ export async function runFrontendWorktreeDiffGate(input) {
152
137
  });
153
138
  const patchSource = baseline
154
139
  ? changedFiles.length > 0
155
- ? await runGit(root, ["diff", "--binary", "HEAD", "--", ...changedFiles])
140
+ ? await run(root, ["diff", "--binary", "HEAD", "--", ...changedFiles])
156
141
  : { code: 0, stdout: "", stderr: "" }
157
142
  : diff;
158
143
  if (patchSource.code !== 0) {
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
- import { spawn } from "node:child_process";
3
2
  import { lstat, readFile, realpath } from "node:fs/promises";
4
3
  import path from "node:path";
4
+ import { runResilientGitCommand } from "../../shared/resilient-git.js";
5
5
  import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
6
6
  /**
7
7
  * Workspace checkpoint for DAG continuation safety.
@@ -13,30 +13,10 @@ function sha256Hex(input) {
13
13
  return createHash("sha256").update(input).digest("hex");
14
14
  }
15
15
  async function runGit(cwd, args) {
16
- return new Promise((resolve, reject) => {
17
- const child = spawn("git", args, {
18
- cwd,
19
- stdio: ["ignore", "pipe", "pipe"],
20
- });
21
- let stdout = "";
22
- let stderr = "";
23
- child.stdout.setEncoding("utf-8");
24
- child.stderr.setEncoding("utf-8");
25
- child.stdout.on("data", (chunk) => {
26
- stdout += chunk;
27
- });
28
- child.stderr.on("data", (chunk) => {
29
- stderr += chunk;
30
- });
31
- child.on("error", reject);
32
- child.on("close", (code) => {
33
- if (code === 0) {
34
- resolve(stdout);
35
- return;
36
- }
37
- reject(new Error(`git ${args.join(" ")} failed: ${stderr.trim() || stdout.trim()}`));
38
- });
39
- });
16
+ const result = await runResilientGitCommand({ cwd, args, readOnly: true });
17
+ if (result.code === 0)
18
+ return result.stdout;
19
+ throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
40
20
  }
41
21
  function normalizeGitPath(value) {
42
22
  const target = value.includes(" -> ")
@@ -137,9 +117,10 @@ async function hashStableGovernanceInputs(repoRoot, taskId) {
137
117
  * When `taskId` is provided, fingerprint includes stable ignored Task Contract inputs.
138
118
  */
139
119
  export async function captureWorkspaceCheckpoint(repoRoot, options) {
120
+ const run = options?.dependencies?.runGit ?? runGit;
140
121
  const capturedAt = (options?.now ?? new Date()).toISOString();
141
- const gitHeadSha = (await runGit(repoRoot, ["rev-parse", "HEAD"])).trim();
142
- const porcelain = await runGit(repoRoot, [
122
+ const gitHeadSha = (await run(repoRoot, ["rev-parse", "HEAD"])).trim();
123
+ const porcelain = await run(repoRoot, [
143
124
  "status",
144
125
  "--porcelain=v1",
145
126
  "--untracked-files=all",
package/harness.json CHANGED
@@ -81,7 +81,7 @@
81
81
  "pi": {
82
82
  "description": "Pi 负责规划、评审、诊断;当 DAG toolProfile=write 时也可做有界写入。模型按复杂度三档配置,格式为 provider/model 字符串(例:wizard-local/grok-4.5);斜杠前为 Pi provider,后为 modelId,勿只写裸 modelId。",
83
83
  "LOW": "wizard-local/minimax-m3",
84
- "MED": "wizard-local/gpt-5.6-sol",
84
+ "MED": "wizard-local/gpt-5.6-terra",
85
85
  "HIGH": "wizard-local/gpt-5.6-sol"
86
86
  }
87
87
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.33.5",
3
+ "version": "0.33.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -224,7 +224,9 @@ loop-agent task advance <task-id> "标题" \
224
224
  --json
225
225
  ```
226
226
 
227
- 默认无 LLM 写 source;工程边界用 flags 显式给出。不要让 AI 直接改写 `source/references/*`。高级用户仍可手工编辑后 `task advance --from-draft`。
227
+ 默认无 LLM 写 source;工程边界用 flags 显式给出。不要让 AI 直接改写 `source/references/*`。
228
+ Product Requirement V3/V4(`product-requirement.md`)由确定性解析消费;`product-analysis` / `requirement-clarification` 不能单独作为可执行 PRD。
229
+ intake 失败时不要在原 PRD 目录生成兼容 md/json;`--from-draft` **仅**人工 reviewed draft / managed 恢复 / 调试,不是 `EMPTY_ACCEPTANCE` 默认恢复路径。
228
230
 
229
231
  ### Feature-study workflow(参考代码 → 轻量实现)
230
232
 
@@ -41,6 +41,19 @@ Task 目录布局:`task-workflow.md`。
41
41
  - 把用户 PRD **整篇改写**进 `需求.md` 后删掉原文路径。
42
42
  - AI 直接 edit `source/references/*`。
43
43
  - 只把 PRD 路径写在聊天里,不落盘到 task 容器。
44
+ - 把 `product-analysis.md` 当可执行 PRD,或把 analysis/clarification 改写成假 AC 以通过 prepare。
45
+ - intake 失败后在原 PRD 目录生成「兼容格式」Markdown / `reviewed-draft.json`,再用 `--from-draft` 绕过(默认禁止)。
46
+ - 把 `--from-draft` 当作 `EMPTY_ACCEPTANCE` 的自动恢复手段。
47
+
48
+ ### Product Analysis 套件
49
+
50
+ | 文件 | role | 用途 |
51
+ |---|---|---|
52
+ | `product-requirement.md` | `requirement` | **唯一**可执行需求事实源(complete 后才进 DAG) |
53
+ | `product-analysis.md` | `analysis` | 分析证据;**不能**单独作为 requirement |
54
+ | `requirement-clarification.md` | `clarification` | 决策追溯;**不能**单独作为 requirement |
55
+
56
+ `task advance --prd` 可多次传入;未写 `--role` 时按路径/frontmatter 推断。intake 未完成时 lifecycle 为 `intake-incomplete`(PRD 已归档,DAG 未生成)。
44
57
 
45
58
  ### 推荐顺序(有 PRD 时)
46
59
 
@@ -37,6 +37,10 @@ loop-agent task status <task-id> --json
37
37
  ```
38
38
 
39
39
  - 用户原始 PRD 用 `task advance --prd <prd>` 归档到 `source/references/`,禁止 AI 改写。**有文件就归档**。
40
+ - Product Analysis 套件职责:`product-requirement.md` 是唯一可执行需求事实源;`product-analysis.md` / `requirement-clarification.md` 仅引用。未显式 `--role` 时按文件名/frontmatter 推断(analysis / clarification / requirement)。
41
+ - 遇到 `PRODUCT_ANALYSIS_NOT_EXECUTABLE` / `PRODUCT_REQUIREMENT_PENDING` / `EMPTY_ACCEPTANCE`:**禁止**改写原始产物,**禁止**在原 PRD 目录生成兼容 Markdown 或 `reviewed-draft.json` 自救;应换正确 requirement 文档或补真实验收标准后重试。
42
+ - `--from-draft` 仅用于人工 reviewed draft / managed 恢复 / 调试,不是 intake 失败的默认恢复路径。
43
+ - 确定性解析失败且缺口为结构性时,`task advance` 可自动做**一次** Semantic Intake(只读 Pi → `artifacts/intake/semantic-draft.json` → prepare);analysis/pending/真缺决策不会调用。可用 `LOOP_AGENT_SEMANTIC_INTAKE=0` 关闭。禁止在原 PRD 目录写 sidecar。
40
44
  - 默认用 `task advance`(带 `--allowed-path` / `--verify`)投影 `需求.md` / `执行约束.md` 与 managed paths;**不要**默认让 LLM/主会话手写两 source。
41
45
  - 无独立 PRD 的微小任务可用 `task advance --from-text ...` escape hatch(见 `source-and-plan-practice.md`)。
42
46
  - 若 `ai_workspace/loop-agent/` 已有权威 plan/spec/PRD,优先 `task advance --prd` 归档并派生薄契约;避免把长 PRD 直接改写成唯一 source。