@tea-agent/loop-agent 0.16.25 → 0.17.0

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 (115) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +14 -3
  3. package/dist/cli/command-definitions.js +43 -0
  4. package/dist/cli/program.js +26 -0
  5. package/dist/commands/dag-approve.js +4 -0
  6. package/dist/commands/dag-resume.js +1 -0
  7. package/dist/commands/dag-validate.js +6 -0
  8. package/dist/commands/operator.js +44 -0
  9. package/dist/commands/task-contract.js +271 -0
  10. package/dist/executors/dag-pi-executor.js +118 -16
  11. package/dist/executors/pi-executor.js +206 -13
  12. package/dist/executors/pi-sdk-executor.js +21 -6
  13. package/dist/executors/shell-executor.js +85 -8
  14. package/dist/executors/shell-presets.js +16 -3
  15. package/dist/executors/shell-write-guard.js +64 -2
  16. package/dist/shared/operator/capabilities.js +255 -0
  17. package/dist/shared/operator/envelope.js +59 -0
  18. package/dist/shared/operator/index.js +4 -0
  19. package/dist/shared/operator/registry.js +38 -0
  20. package/dist/shared/operator/types.js +5 -0
  21. package/dist/task/contract/adopt.js +166 -0
  22. package/dist/task/contract/apply.js +326 -0
  23. package/dist/task/contract/canonicalize.js +60 -0
  24. package/dist/task/contract/constants.js +29 -0
  25. package/dist/task/contract/diff.js +177 -0
  26. package/dist/task/contract/hash.js +42 -0
  27. package/dist/task/contract/import-revision.js +96 -0
  28. package/dist/task/contract/index.js +17 -0
  29. package/dist/task/contract/journal.js +155 -0
  30. package/dist/task/contract/lock.js +153 -0
  31. package/dist/task/contract/observe.js +296 -0
  32. package/dist/task/contract/paths.js +19 -0
  33. package/dist/task/contract/project.js +170 -0
  34. package/dist/task/contract/recover.js +312 -0
  35. package/dist/task/contract/request-ledger.js +37 -0
  36. package/dist/task/contract/schema.js +151 -0
  37. package/dist/task/contract/transaction.js +160 -0
  38. package/dist/task/contract/types.js +1 -0
  39. package/dist/task/contract/validate-draft.js +106 -0
  40. package/dist/task/index.js +3 -0
  41. package/dist/task/operator/capabilities.js +6 -0
  42. package/dist/task/operator/envelope.js +2 -0
  43. package/dist/task/operator/index.js +5 -0
  44. package/dist/task/operator/registry.js +2 -0
  45. package/dist/task/operator/types.js +1 -0
  46. package/dist/task/runtime.js +5 -1
  47. package/dist/task/source-references.js +7 -0
  48. package/dist/worker/cli.js +150 -32
  49. package/dist/worker/console/app-data.js +185 -0
  50. package/dist/worker/console/dag-confirmation.js +313 -0
  51. package/dist/worker/console/doctor.js +169 -0
  52. package/dist/worker/console/draft-store.js +80 -0
  53. package/dist/worker/console/index.js +15 -0
  54. package/dist/worker/console/interview/assessment.js +67 -0
  55. package/dist/worker/console/interview/session.js +100 -0
  56. package/dist/worker/console/interview/tools.js +109 -0
  57. package/dist/worker/console/loopback.js +16 -0
  58. package/dist/worker/console/observe-health-match.js +174 -0
  59. package/dist/worker/console/observe-link.js +33 -0
  60. package/dist/worker/console/operation-runner.js +166 -0
  61. package/dist/worker/console/operation-sse.js +158 -0
  62. package/dist/worker/console/operation-store.js +147 -0
  63. package/dist/worker/console/operator-actions.js +769 -0
  64. package/dist/worker/console/pi-readiness.js +94 -0
  65. package/dist/worker/console/recovery-cta.js +133 -0
  66. package/dist/worker/console/repo-fingerprint.js +29 -0
  67. package/dist/worker/console/resource-loader.js +95 -0
  68. package/dist/worker/console/routes.js +368 -0
  69. package/dist/worker/console/security.js +126 -0
  70. package/dist/worker/console/server.js +149 -0
  71. package/dist/worker/console/sibling-controller.js +28 -0
  72. package/dist/worker/console/static/assets/index-CbnMgdWa.js +9 -0
  73. package/dist/worker/console/static/assets/index-Dnj0RVs8.css +1 -0
  74. package/dist/worker/console/static/index.html +13 -0
  75. package/dist/worker/console/vite.config.js +27 -0
  76. package/dist/worker/delivery/git-transaction.js +43 -8
  77. package/dist/worker/observe/health.js +57 -0
  78. package/dist/worker/observe/paths.js +81 -0
  79. package/dist/worker/observe/routes.js +142 -27
  80. package/dist/worker/observe/spec-evidence.js +84 -0
  81. package/dist/worker/observe/static/api.js +23 -0
  82. package/dist/worker/observe/static/state.js +26 -0
  83. package/dist/worker/observe/static/styles.css +10 -0
  84. package/dist/worker/observe/static/views/dag-inspector.js +173 -6
  85. package/dist/workflows/dag/backend-test-analysis-contract.js +34 -9
  86. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -0
  87. package/dist/workflows/dag/frontend-repair.js +1 -10
  88. package/dist/workflows/dag/init-hybrid.js +372 -115
  89. package/dist/workflows/dag/node-execution.js +57 -6
  90. package/dist/workflows/dag/project-governance-context.js +508 -0
  91. package/dist/workflows/dag/prompt.js +46 -1
  92. package/dist/workflows/dag/retry-policy.js +16 -1
  93. package/dist/workflows/dag/runner.js +9 -0
  94. package/dist/workflows/dag/skill-snapshot.js +1 -0
  95. package/dist/workflows/dag/task-contract-binding.js +138 -0
  96. package/dist/workflows/dag/types.js +84 -10
  97. package/dist/workflows/dag/validate.js +53 -7
  98. package/docs/README.md +2 -0
  99. package/docs/architecture/evolution.md +2 -0
  100. package/docs/architecture/system-overview.md +6 -0
  101. package/docs/architecture/worker-and-feature.md +7 -0
  102. package/docs/templates/agent-dag.schema.json +64 -2
  103. package/docs/templates/agent-dag.supervised-implementation.json +1 -0
  104. package/docs/templates/backend-test-dag.classify.prompt.md +1 -1
  105. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +5 -5
  106. package/docs/templates/backend-test-dag.json +26 -154
  107. package/docs/templates/backend-test-dag.retrospect.prompt.md +1 -1
  108. package/docs/templates/backend-test-dag.review-cases.prompt.md +2 -2
  109. package/package.json +8 -2
  110. package/skills/agent-worker/SKILL.md +1 -0
  111. package/skills/agent-worker/references/agent-worker-operator.md +3 -2
  112. package/skills/frontend-design-review/SKILL.md +25 -16
  113. package/skills/frontend-implementation/references/node-contracts.md +5 -5
  114. package/skills/loop-agent/references/command-reference.md +48 -1
  115. package/skills/loop-agent/references/hybrid-dag.md +4 -4
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Pi readiness gate for Official Console Happy Path.
3
+ * setup-required | ready | degraded — no pure form fallback.
4
+ */
5
+ const SETUP_BLOCKED = Object.freeze([
6
+ "newTask",
7
+ "importPrd",
8
+ "contractApply",
9
+ "contractAdopt",
10
+ "dagRunTask",
11
+ "runDag",
12
+ "prepareDagConfirmation",
13
+ "interviewStart",
14
+ "interviewTurn",
15
+ ]);
16
+ const DEGRADED_BLOCKED = Object.freeze([
17
+ "contractApply",
18
+ "contractAdopt",
19
+ "dagRunTask",
20
+ "runDag",
21
+ // new Interview may resume; brand-new apply/run still blocked
22
+ ]);
23
+ export function classifyPiReadiness(probe) {
24
+ if (!probe.sdkAvailable ||
25
+ !probe.authReady ||
26
+ !probe.modelsAvailable ||
27
+ !probe.interviewSessionOk) {
28
+ // Temporary provider issues after prior readiness → degraded when only
29
+ // session fails but sdk/auth/models were previously ok is hard to know;
30
+ // rule: missing core setup → setup-required; only session flaky → degraded.
31
+ if (!probe.sdkAvailable || !probe.authReady || !probe.modelsAvailable) {
32
+ return "setup-required";
33
+ }
34
+ return "degraded";
35
+ }
36
+ return "ready";
37
+ }
38
+ export function buildPiReadinessReport(probe, now = new Date().toISOString()) {
39
+ const state = classifyPiReadiness(probe);
40
+ const blockedActions = state === "setup-required"
41
+ ? SETUP_BLOCKED
42
+ : state === "degraded"
43
+ ? DEGRADED_BLOCKED
44
+ : Object.freeze([]);
45
+ return {
46
+ state,
47
+ probe,
48
+ generatedAt: now,
49
+ blockedActions,
50
+ };
51
+ }
52
+ export function isActionBlockedByReadiness(state, action) {
53
+ if (state === "ready")
54
+ return false;
55
+ if (state === "setup-required") {
56
+ return SETUP_BLOCKED.includes(action);
57
+ }
58
+ return DEGRADED_BLOCKED.includes(action);
59
+ }
60
+ /**
61
+ * Default probe for process boot. Without a real Pi ModelRuntime injection,
62
+ * Official Console is setup-required (no form fallback).
63
+ * Tests and server may inject a readiness probe override.
64
+ */
65
+ export async function probePiReadiness(options) {
66
+ if (options?.override) {
67
+ return buildPiReadinessReport(options.override);
68
+ }
69
+ // Phase 2: real Pi SDK/auth/model discovery is optional injection.
70
+ // Default fail-closed to setup-required so Happy Path cannot skip Interview.
71
+ const probe = {
72
+ sdkAvailable: false,
73
+ authReady: false,
74
+ modelsAvailable: false,
75
+ interviewSessionOk: false,
76
+ details: {
77
+ sdkMessage: "Pi SDK/ModelRuntime not injected; configure Interview model/auth (setup-required)",
78
+ authMessage: "no credential probe configured",
79
+ modelsMessage: "no model list available",
80
+ sessionMessage: "Interview session preflight not run",
81
+ },
82
+ };
83
+ return buildPiReadinessReport(probe);
84
+ }
85
+ export function readinessGateFailure(report, action) {
86
+ if (!isActionBlockedByReadiness(report.state, action))
87
+ return undefined;
88
+ return {
89
+ code: "PI_SETUP_REQUIRED",
90
+ message: `action "${action}" is blocked while pi readiness is ${report.state} (no pure form fallback)`,
91
+ state: report.state,
92
+ action,
93
+ };
94
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Recovery UI CTA matrix (design §5.5.3).
3
+ * Pure table-driven policy — no Cancel, no primary "edit code" CTA.
4
+ */
5
+ const CTA = {
6
+ report: {
7
+ id: "report",
8
+ label: "查看报告",
9
+ action: "status",
10
+ commandHint: "loop-agent dag report --run-id <run-id>",
11
+ },
12
+ doctor: {
13
+ id: "doctor",
14
+ label: "Doctor 诊断",
15
+ action: "doctor",
16
+ commandHint: "agent-worker console doctor --repo .",
17
+ },
18
+ decision: {
19
+ id: "decision",
20
+ label: "人工决策",
21
+ commandHint: "loop-agent dag doctor --run-id <run-id> --markdown",
22
+ },
23
+ resume: {
24
+ id: "resume",
25
+ label: "恢复运行",
26
+ commandHint: "loop-agent run-dag --resume --run-id <run-id>",
27
+ },
28
+ reconcile: {
29
+ id: "reconcile",
30
+ label: "对账 / reconcile",
31
+ requiresReason: true,
32
+ commandHint: "loop-agent dag reconcile-run --run-id <run-id> --reason <structured-reason>",
33
+ },
34
+ regenerate: {
35
+ id: "regenerate",
36
+ label: "重新生成契约/计划",
37
+ commandHint: "loop-agent dag run-task <task-id> --profile auto",
38
+ },
39
+ observeLink: {
40
+ id: "observeLink",
41
+ label: "打开 Observe 深链",
42
+ action: "observeLink",
43
+ commandHint: "agent-worker observe serve --repo . --port 8787",
44
+ },
45
+ };
46
+ const MATRIX = {
47
+ "validation-failed": ["report", "doctor", "regenerate"],
48
+ "contract-drift": ["report", "doctor", "decision", "reconcile", "regenerate"],
49
+ "runtime-mismatch": ["report", "doctor", "reconcile", "regenerate"],
50
+ "pause-on-human": ["decision", "resume", "report", "doctor"],
51
+ "verification-failed": ["report", "doctor", "resume", "regenerate"],
52
+ "orphan-unknown": ["report", "doctor", "reconcile"],
53
+ "terminal-failed": ["report", "doctor", "resume", "regenerate"],
54
+ "terminal-succeeded": ["report", "observeLink"],
55
+ "needs-reconcile": ["report", "doctor", "reconcile", "regenerate"],
56
+ };
57
+ const FORBIDDEN_LABELS = [
58
+ "直接改代码",
59
+ "edit code",
60
+ "Cancel",
61
+ "cancel operation",
62
+ ];
63
+ /**
64
+ * Return allowed recovery CTAs for a fact/failure class.
65
+ * Never includes Cancel or primary "edit code".
66
+ */
67
+ export function allowedRecoveryCtas(factClass) {
68
+ const ids = MATRIX[factClass] ?? MATRIX["orphan-unknown"];
69
+ return ids.map((id) => CTA[id]);
70
+ }
71
+ /**
72
+ * Negative product invariants for recovery matrix (test + docs).
73
+ */
74
+ export function recoveryMatrixInvariants() {
75
+ const allCtaIds = Object.keys(CTA);
76
+ const factClasses = Object.keys(MATRIX);
77
+ return {
78
+ forbiddenLabels: FORBIDDEN_LABELS,
79
+ allCtaIds,
80
+ factClasses,
81
+ };
82
+ }
83
+ /**
84
+ * Best-effort classification from operation/DAG surface strings for MVP UI.
85
+ * Prefer structured fields when available; unknown → orphan-unknown.
86
+ */
87
+ export function classifyRecoveryFact(input) {
88
+ if (input.needsReconcile)
89
+ return "needs-reconcile";
90
+ const code = (input.errorCode ?? "").toLowerCase();
91
+ const state = (input.operationState ?? "").toLowerCase();
92
+ const dag = (input.dagStatus ?? "").toLowerCase();
93
+ if (code.includes("validation") ||
94
+ code.includes("invalid") ||
95
+ state.includes("validation")) {
96
+ return "validation-failed";
97
+ }
98
+ if (code.includes("contract") ||
99
+ code.includes("drift") ||
100
+ code.includes("binding")) {
101
+ return "contract-drift";
102
+ }
103
+ if (code.includes("runtime") ||
104
+ code.includes("mismatch") ||
105
+ code.includes("fingerprint")) {
106
+ return "runtime-mismatch";
107
+ }
108
+ if (code.includes("human") ||
109
+ state.includes("paused") ||
110
+ dag.includes("paused") ||
111
+ state.includes("awaiting")) {
112
+ return "pause-on-human";
113
+ }
114
+ if (code.includes("verif") || state.includes("verif")) {
115
+ return "verification-failed";
116
+ }
117
+ if (state === "succeeded" ||
118
+ state === "completed" ||
119
+ dag === "succeeded" ||
120
+ dag === "completed") {
121
+ return "terminal-succeeded";
122
+ }
123
+ if (state === "failed" ||
124
+ state.includes("fail") ||
125
+ dag === "failed" ||
126
+ code.includes("fail")) {
127
+ return "terminal-failed";
128
+ }
129
+ if (code.includes("reconcil") || state.includes("reconcil")) {
130
+ return "needs-reconcile";
131
+ }
132
+ return "orphan-unknown";
133
+ }
@@ -0,0 +1,29 @@
1
+ import { createHash } from "node:crypto";
2
+ import { realpathSync } from "node:fs";
3
+ import path from "node:path";
4
+ /**
5
+ * Normalize a worktree root for repoFingerprintV1:
6
+ * realpath → Windows drive letter uppercased → `/` separators → strip trailing `/`
7
+ * (except a bare root). No extra case-folding on macOS/Windows.
8
+ */
9
+ export function normalizeWorktreeRealpath(repoRoot) {
10
+ let resolved = realpathSync(path.resolve(repoRoot));
11
+ if (/^[a-zA-Z]:/.test(resolved)) {
12
+ resolved = resolved[0].toUpperCase() + resolved.slice(1);
13
+ }
14
+ resolved = resolved.replace(/\\/g, "/");
15
+ if (resolved.length > 1 && resolved.endsWith("/")) {
16
+ resolved = resolved.replace(/\/+$/, "");
17
+ }
18
+ return resolved;
19
+ }
20
+ /**
21
+ * Design §5.1: sha256("loop-console-repo-v1\0" + normalizedWorktreeRealpath)
22
+ * Returns lowercase hex digest (no sha256: prefix).
23
+ */
24
+ export function repoFingerprintV1(repoRoot) {
25
+ const normalized = normalizeWorktreeRealpath(repoRoot);
26
+ return createHash("sha256")
27
+ .update(`loop-console-repo-v1\0${normalized}`, "utf8")
28
+ .digest("hex");
29
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Official Console ResourceLoader (Phase 2).
3
+ * Closed surface: no edit/write/bash/full-tools business tools.
4
+ * Runtime env MUST NOT restore denied tools.
5
+ * Requirement Interview uses a stricter allowlist (see interview/tools.ts).
6
+ */
7
+ import { createInterviewResourceLoader, INTERVIEW_ALLOWED_TOOLS, INTERVIEW_DENIED_TOOLS, authorizeInterviewTool, } from "./interview/tools.js";
8
+ export const OFFICIAL_DENIED_TOOL_IDS = Object.freeze([
9
+ "edit",
10
+ "write",
11
+ "bash",
12
+ "full-tools",
13
+ "full_tools",
14
+ "coding-chat",
15
+ "coding_chat",
16
+ "shell",
17
+ "apply_patch",
18
+ "apply-patch",
19
+ // Interview / operator mutation tools must never appear as free coding tools
20
+ "contractApply",
21
+ "contract_apply",
22
+ "runDag",
23
+ "run_dag",
24
+ "reconcile",
25
+ ]);
26
+ /** Official Console surface allows Interview-scoped tools only when Interview session is active. */
27
+ export const OFFICIAL_ALLOWED_TOOL_IDS = Object.freeze([
28
+ ...INTERVIEW_ALLOWED_TOOLS,
29
+ ]);
30
+ const DENIED = new Set(OFFICIAL_DENIED_TOOL_IDS.map((id) => id.toLowerCase()));
31
+ const ALLOWED = new Set(OFFICIAL_ALLOWED_TOOL_IDS.map((id) => id.toLowerCase()));
32
+ /** Official allowed tools: Interview closed set (no mutation operator tools). */
33
+ export function listOfficialTools() {
34
+ return OFFICIAL_ALLOWED_TOOL_IDS;
35
+ }
36
+ export function isOfficialToolDenied(toolId) {
37
+ const id = toolId.trim().toLowerCase();
38
+ if (DENIED.has(id))
39
+ return true;
40
+ const compact = id.replace(/[-_]/g, "");
41
+ for (const d of DENIED) {
42
+ if (d.replace(/[-_]/g, "") === compact)
43
+ return true;
44
+ }
45
+ return false;
46
+ }
47
+ /**
48
+ * Attempt to activate a tool on the Official Console surface.
49
+ * Write-class, full-tools, and Interview-forbidden mutation ids are always rejected.
50
+ */
51
+ export function tryActivateTool(toolId) {
52
+ const id = toolId.trim();
53
+ if (!id) {
54
+ return {
55
+ ok: false,
56
+ code: "tool-denied",
57
+ toolId: id,
58
+ message: "empty tool id is not allowed on Official Console surface",
59
+ };
60
+ }
61
+ if (isOfficialToolDenied(id)) {
62
+ return {
63
+ ok: false,
64
+ code: "tool-denied",
65
+ toolId: id,
66
+ message: `tool "${id}" is denied on Official Loop Operator Console (no edit/write/bash/full-tools/contractApply/runDag/reconcile)`,
67
+ };
68
+ }
69
+ if (!ALLOWED.has(id.toLowerCase())) {
70
+ return {
71
+ ok: false,
72
+ code: "tool-denied",
73
+ toolId: id,
74
+ message: `tool "${id}" is not registered on Official Console ResourceLoader`,
75
+ };
76
+ }
77
+ return { ok: true, toolId: id };
78
+ }
79
+ export function assertOfficialToolAllowed(toolId) {
80
+ const result = tryActivateTool(toolId);
81
+ if (!result.ok) {
82
+ throw new Error(result.message);
83
+ }
84
+ }
85
+ export function createClosedResourceLoader() {
86
+ return {
87
+ mode: "official-closed",
88
+ deniedToolIds: OFFICIAL_DENIED_TOOL_IDS,
89
+ allowedToolIds: OFFICIAL_ALLOWED_TOOL_IDS,
90
+ listTools: listOfficialTools,
91
+ tryActivateTool,
92
+ allowsEnvToolRestore: false,
93
+ };
94
+ }
95
+ export { createInterviewResourceLoader, INTERVIEW_ALLOWED_TOOLS, INTERVIEW_DENIED_TOOLS, authorizeInterviewTool, };