@tea-agent/loop-agent 0.36.4-beta.0 → 0.37.1-beta.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 (118) hide show
  1. package/AGENTS.md +2 -6
  2. package/CHANGELOG.md +72 -163
  3. package/README.md +1 -1
  4. package/bin/loop-agent.js +1 -37
  5. package/dist/application/task-lifecycle/advance.js +0 -1
  6. package/dist/application/task-lifecycle/observe.js +0 -15
  7. package/dist/application/task-lifecycle/plan-transitions.js +0 -15
  8. package/dist/cli/command-definitions.js +0 -7
  9. package/dist/cli/program.js +3 -8
  10. package/dist/commands/init-upgrade.js +19 -351
  11. package/dist/commands/init.js +67 -14
  12. package/dist/commands/run-dag-progress.js +0 -14
  13. package/dist/commands/task-advance.js +3 -33
  14. package/dist/executors/dag-pi-executor.js +30 -47
  15. package/dist/executors/pi-sdk-executor.js +0 -34
  16. package/dist/executors/shell-executor.js +95 -6
  17. package/dist/infrastructure/harness/atomic-write.js +1 -2
  18. package/dist/shared/operator/capabilities.js +13 -200
  19. package/dist/shared/package-metadata.js +0 -42
  20. package/dist/task/frontend-project-capability.js +12 -162
  21. package/dist/task/source-prepare/completeness.js +0 -17
  22. package/dist/task/source-prepare/parse-intent.js +1 -15
  23. package/dist/task/source-prepare/semantic-intake.js +23 -144
  24. package/dist/worker/console/chat/artifact-card.js +1 -8
  25. package/dist/worker/console/chat/chat-event-store.js +0 -61
  26. package/dist/worker/console/chat/human-gate-card.js +1 -9
  27. package/dist/worker/console/chat/operation-card.js +2 -71
  28. package/dist/worker/console/chat/pi-runtime.js +62 -86
  29. package/dist/worker/console/chat/routes.js +8 -36
  30. package/dist/worker/console/chat/session-store.js +0 -3
  31. package/dist/worker/console/chat/shortcuts.js +7 -9
  32. package/dist/worker/console/chat/turn-process.js +23 -97
  33. package/dist/worker/console/chat/workspace-landing.js +1 -2
  34. package/dist/worker/console/operation-runner.js +6 -155
  35. package/dist/worker/console/operator-actions.js +13 -373
  36. package/dist/worker/console/operator-user-error.js +0 -4
  37. package/dist/worker/console/recovery-cta.js +3 -50
  38. package/dist/worker/console/static/assets/index-HX1pbOyl.css +1 -0
  39. package/dist/worker/console/static/assets/index-M0BLEBfh.js +56 -0
  40. package/dist/worker/console/static/index.html +2 -2
  41. package/dist/worker/console/static-src/app/console-types.js +9 -13
  42. package/dist/worker/console/static-src/app/useOperatorActions.js +2 -4
  43. package/dist/worker/console/static-src/app/useRecoveryActions.js +56 -65
  44. package/dist/worker/console/static-src/app/useRecoveryConsole.js +1 -73
  45. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +30 -89
  46. package/dist/worker/console/static-src/operator-chat/refs.js +0 -3
  47. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  48. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +11 -30
  49. package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -12
  50. package/dist/worker/console/static-src/operator-chat/useComposer.js +8 -45
  51. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -7
  52. package/dist/worker/loop-agent/loop-agent-client.js +3 -17
  53. package/dist/worker/observability/read-model.js +0 -20
  54. package/dist/worker/observe/spec-evidence.js +8 -3
  55. package/dist/worker/observe/static/operator-chrome.d.ts +0 -1
  56. package/dist/worker/observe/static/operator-chrome.js +1 -6
  57. package/dist/worker/observe/static/views/dag-inspector.js +71 -6
  58. package/dist/worker/observe/static/views/dag.js +0 -12
  59. package/dist/worker/preflight.js +1 -2
  60. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +707 -37
  61. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  62. package/dist/workflows/dag/backend-test-markdown-workflow.js +109 -7
  63. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  64. package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
  65. package/dist/workflows/dag/backend-test-scenario-param.js +890 -164
  66. package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
  67. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  68. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
  69. package/dist/workflows/dag/failure-routing.js +4 -9
  70. package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
  71. package/dist/workflows/dag/frontend-prewrite-gate.js +62 -394
  72. package/dist/workflows/dag/frontend-repair.js +18 -219
  73. package/dist/workflows/dag/frontend-verification-trace.js +32 -47
  74. package/dist/workflows/dag/init-hybrid.js +86 -238
  75. package/dist/workflows/dag/lifecycle.js +0 -4
  76. package/dist/workflows/dag/node-execution.js +6 -105
  77. package/dist/workflows/dag/recovery-recommendation.js +0 -58
  78. package/dist/workflows/dag/report.js +0 -6
  79. package/dist/workflows/dag/retry-policy.js +0 -11
  80. package/dist/workflows/dag/runner.js +12 -314
  81. package/dist/workflows/dag/scheduler.js +3 -257
  82. package/dist/workflows/dag/types.js +9 -132
  83. package/dist/workflows/dag/validate.js +2 -7
  84. package/docs/README.md +3 -3
  85. package/docs/architecture/evolution.md +67 -102
  86. package/docs/templates/backend-test-dag.json +50 -32
  87. package/docs/templates/frontend-design-contract.md +14 -34
  88. package/docs/templates/frontend-task-constraints.md +13 -33
  89. package/docs/templates/frontend-task-requirement.md +20 -54
  90. package/docs/templates/init-managed-agents.md +2 -5
  91. package/harness.json +2 -2
  92. package/package.json +3 -4
  93. package/skills/loop-agent/SKILL.md +0 -1
  94. package/skills/loop-agent/references/command-reference.md +0 -3
  95. package/dist/build-stamp.json +0 -6
  96. package/dist/commands/dag-request-interrupt.js +0 -20
  97. package/dist/worker/console/chat/assistant-content.js +0 -121
  98. package/dist/worker/console/chat/semantic-activity.js +0 -471
  99. package/dist/worker/console/operation-run-facts.js +0 -190
  100. package/dist/worker/console/operation-wait.js +0 -355
  101. package/dist/worker/console/recovery-error-copy.js +0 -198
  102. package/dist/worker/console/static/assets/index-D83DYAFG.css +0 -1
  103. package/dist/worker/console/static/assets/index-IXm7oYjL.js +0 -59
  104. package/dist/worker/console/static-src/operator-chat/activity-journey.js +0 -125
  105. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  106. package/dist/worker/console/static-src/operator-chat/activity-references.js +0 -20
  107. package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +0 -24
  108. package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +0 -141
  109. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
  110. package/dist/worker/observability/interrupt-eligibility.js +0 -264
  111. package/dist/workflows/dag/contract-output-registry.js +0 -14
  112. package/dist/workflows/dag/contract-validator-registrations.js +0 -8
  113. package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
  114. package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
  115. package/dist/workflows/dag/frontend-recovery-run.js +0 -539
  116. package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
  117. package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
  118. package/dist/workflows/dag/interrupt-request.js +0 -559
@@ -13,13 +13,6 @@ const CONTROLLER_PROTOCOL_CAPABILITIES = [
13
13
  "worker-attempt-revision-v1",
14
14
  "recovery-decision-v2",
15
15
  ];
16
- function officialModelCallable(coverage, kind) {
17
- if (coverage === "human-gated-required")
18
- return "prepare-only";
19
- if (coverage === "advanced" && kind !== "read")
20
- return "never";
21
- return "always";
22
- }
23
16
  const officialAction = (action, cli, kind, coverage) => ({
24
17
  action,
25
18
  cli,
@@ -58,7 +51,11 @@ const officialAction = (action, cli, kind, coverage) => ({
58
51
  ]
59
52
  : []),
60
53
  ],
61
- modelCallable: officialModelCallable(coverage, kind),
54
+ modelCallable: coverage === "human-gated-required"
55
+ ? "prepare-only"
56
+ : coverage === "advanced" && kind !== "read"
57
+ ? "never"
58
+ : "always",
62
59
  humanConfirmation: coverage === "human-gated-required" ? "required" : "none",
63
60
  });
64
61
  const OFFICIAL_ACTIONS = [
@@ -145,13 +142,13 @@ const OFFICIAL_ACTIONS = [
145
142
  ["stats", "loop-agent stats", "read", "model-callable"],
146
143
  [
147
144
  "promoteRun",
148
- "loop-agent task advance",
145
+ "loop-agent task advance <id> --json",
149
146
  "mutation",
150
147
  "model-callable",
151
148
  ],
152
149
  [
153
150
  "closeoutTask",
154
- "loop-agent task advance",
151
+ "loop-agent task advance <id> --json",
155
152
  "mutation",
156
153
  "model-callable",
157
154
  ],
@@ -448,49 +445,6 @@ export function buildOperatorCapabilitiesDocument() {
448
445
  modelCallable: "always",
449
446
  humanConfirmation: "none",
450
447
  },
451
- {
452
- action: "operationWait",
453
- cli: "console canonical operation wait (read-only event-driven long poll)",
454
- kind: "read",
455
- inputSchemaVersion: 1,
456
- resultSchemaVersion: 1,
457
- resultPolicy: { readOnly: true, bounded: true, redacted: true },
458
- envelopeSchemaVersion: 1,
459
- requiredErrorCodes: [
460
- "NOT_FOUND",
461
- "INVALID_INPUT",
462
- "EVENT_CURSOR_EXPIRED",
463
- ],
464
- description: "Read-only event-driven wait on the canonical operation event ring. Default wakeOn=meaningful: heartbeats never settle the wait and are never returned as event payload (nextSeq still covers them); state/result/stdout/stderr/reconcile/error settle immediately; terminal or needs-reconcile flushes immediately. Every subsequent wait MUST pass afterSeq equal to the previous response nextSeq; omitting or reusing an older cursor replays already-consumed startup events. The response includes recommendedNextCall with the safe cursor. wakeOn=all (diagnostic compatibility) resolves on the first new event including heartbeats. maxWaitMs elapsing returns timedOut:true (a success summary, not a command failure).",
465
- inputParams: [
466
- {
467
- name: "operationId",
468
- type: "string",
469
- required: true,
470
- description: "canonical operation id",
471
- },
472
- {
473
- name: "afterSeq",
474
- type: "number",
475
- required: false,
476
- description: "event cursor; first wait may use 0, every later wait MUST use the previous response nextSeq (>= 0)",
477
- },
478
- {
479
- name: "maxWaitMs",
480
- type: "number",
481
- required: false,
482
- description: "bounded wait budget; clamped server-side (default 180000, floor 60000)",
483
- },
484
- {
485
- name: "wakeOn",
486
- type: "string",
487
- required: false,
488
- description: "wake policy: meaningful (default; heartbeats are cursor-only liveness) | all (first event settles, diagnostics)",
489
- },
490
- ],
491
- modelCallable: "always",
492
- humanConfirmation: "none",
493
- },
494
448
  {
495
449
  action: "contractShow",
496
450
  cli: "loop-agent task status <taskId> --json",
@@ -942,74 +896,6 @@ export function buildOperatorCapabilitiesDocument() {
942
896
  modelCallable: "always",
943
897
  humanConfirmation: "none",
944
898
  },
945
- {
946
- action: "taskAdvance",
947
- cli: "loop-agent task advance <taskId> [title] --task-kind <kind> --allowed-path <glob> --forbidden-path <glob> --verify <label:command> --json",
948
- kind: "long-running",
949
- inputSchemaVersion: 1,
950
- resultSchemaVersion: 1,
951
- envelopeSchemaVersion: 1,
952
- requiredErrorCodes: [
953
- ...COMMON_MUTATION_ERRORS,
954
- "INVALID_INPUT",
955
- ],
956
- description: "Canonical typed task advance mutation. Use after task creation/PRD import to apply explicit taskKind and engineering boundaries, generate + strict-validate the DAG, and stop at the writeSet gate. Never pass placeholder ids such as <id>. A successful child process may still return a blocked business outcome; inspect lifecycleState, blockers, gate, and next.",
957
- inputParams: [
958
- {
959
- name: "taskId",
960
- type: "string",
961
- required: true,
962
- description: "canonical YYYY-MM-DD-<slug> task id; placeholders are rejected",
963
- },
964
- {
965
- name: "title",
966
- type: "string",
967
- required: false,
968
- description: "optional task title for create/first advance",
969
- },
970
- {
971
- name: "taskKind",
972
- type: "string",
973
- required: false,
974
- description: "business DAG template; ordinary fixes/docs use standard",
975
- },
976
- {
977
- name: "featureId",
978
- type: "string",
979
- required: false,
980
- description: "required only by feature-scoped task kinds such as knowledge-sync",
981
- },
982
- {
983
- name: "profile",
984
- type: "string",
985
- required: false,
986
- description: "governance profile auto|minimal|standard|reviewed|supervised",
987
- },
988
- {
989
- name: "allowedPaths",
990
- type: "array",
991
- itemsType: "string",
992
- required: false,
993
- description: "explicit writer boundary globs",
994
- },
995
- {
996
- name: "forbiddenPaths",
997
- type: "array",
998
- itemsType: "string",
999
- required: false,
1000
- description: "explicit forbidden path globs",
1001
- },
1002
- {
1003
- name: "verifyCommands",
1004
- type: "array",
1005
- itemsType: "string",
1006
- required: false,
1007
- description: "verification entries as label:command strings",
1008
- },
1009
- ],
1010
- modelCallable: "always",
1011
- humanConfirmation: "none",
1012
- },
1013
899
  {
1014
900
  action: "dagRunTask",
1015
901
  cli: "loop-agent task advance <taskId> --profile auto --dag-output <path> --json",
@@ -1079,7 +965,7 @@ export function buildOperatorCapabilitiesDocument() {
1079
965
  "CONTROLLER_MISMATCH",
1080
966
  "INVALID_INPUT",
1081
967
  ],
1082
- description: "Consume a single-use execution receipt and execute the reviewed DAG (prefer task advance --approve-gate when gate token present). accepted/queued/running/operationId are NOT completion — supervise via operationGet/status/dagReport/dagDoctor/operationWait.",
968
+ description: "Consume a single-use execution receipt and execute the reviewed DAG (prefer task advance --approve-gate when gate token present). accepted/queued/running/operationId are NOT completion — supervise via operationGet/status/dagReport/dagDoctor.",
1083
969
  inputParams: [
1084
970
  {
1085
971
  name: "executionId",
@@ -1102,7 +988,7 @@ export function buildOperatorCapabilitiesDocument() {
1102
988
  resultSchemaVersion: 1,
1103
989
  envelopeSchemaVersion: 1,
1104
990
  requiredErrorCodes: ["INVALID_INPUT", "NOT_FOUND"],
1105
- description: "Read-only R1 subgraph plan (no mutation). Call before dagRerun. Prefer primaryFailure.nodeId from dagReport. If eligible=false (writer/decision/fingerprint, unsafe shell, worker-managed, workspace/controller drift), do not force execute — Worker-owned runs use workerTaskRetry; standalone runs use standaloneTaskRerun or same-task advance after fixing the root cause (contract changes must be adopted first). Returns planHash required by execute.",
991
+ description: "Read-only R1 subgraph plan (no mutation). Call before dagRerun. Prefer primaryFailure.nodeId from dagReport. If eligible=false (writer/decision/fingerprint), do not force execute — fall back to standaloneTaskRerun or same-task advance. Returns planHash required by execute.",
1106
992
  inputParams: [
1107
993
  {
1108
994
  name: "runId",
@@ -1165,73 +1051,6 @@ export function buildOperatorCapabilitiesDocument() {
1165
1051
  modelCallable: "always",
1166
1052
  humanConfirmation: "none",
1167
1053
  },
1168
- {
1169
- action: "dagInterruptEligibility",
1170
- cli: "console dag-interrupt-eligibility",
1171
- kind: "read",
1172
- inputSchemaVersion: 1,
1173
- resultSchemaVersion: 1,
1174
- envelopeSchemaVersion: 1,
1175
- requiredErrorCodes: ["INVALID_INPUT", "NOT_FOUND"],
1176
- description: "Read-only eligibility for cooperative live-DAG interrupt. v1 only Console-owned active running operations. Worker-managed / unknown owner / orphaned / terminal runs return eligible=false with stable reasonCodes. Does not create a Human Gate receipt or mutate the run.",
1177
- inputParams: [
1178
- {
1179
- name: "runId",
1180
- type: "string",
1181
- required: true,
1182
- description: "active dag run id to inspect for interrupt eligibility",
1183
- },
1184
- ],
1185
- modelCallable: "always",
1186
- humanConfirmation: "none",
1187
- },
1188
- {
1189
- action: "dagRequestInterrupt",
1190
- cli: "loop-agent dag request-interrupt --run-id <id> --request-id <id> --target-operation-id <id> --reason-code <code> --reason <detail> --json",
1191
- kind: "mutation",
1192
- inputSchemaVersion: 1,
1193
- resultSchemaVersion: 1,
1194
- envelopeSchemaVersion: 1,
1195
- requiredErrorCodes: [
1196
- ...COMMON_MUTATION_ERRORS,
1197
- "HUMAN_CONFIRMATION_REQUIRED",
1198
- "RUN_NOT_ACTIVE",
1199
- "RUN_NOT_CONSOLE_OWNED",
1200
- "RUNNER_IDENTITY_MISMATCH",
1201
- "INTERRUPT_ALREADY_PENDING",
1202
- "RUN_ALREADY_TERMINAL",
1203
- "INTERRUPT_NEEDS_RECONCILE",
1204
- ],
1205
- description: "Request cooperative abort of a Console-owned running DAG. Success means interrupt.json is requested, not that the run has already stopped. Browser Human Gate only; model must not call this action.",
1206
- inputParams: [
1207
- {
1208
- name: "runId",
1209
- type: "string",
1210
- required: true,
1211
- description: "active dag run id",
1212
- },
1213
- {
1214
- name: "reasonCode",
1215
- type: "string",
1216
- required: true,
1217
- description: "requirements-changed | suspected-runaway | resource-protection | operator-request | other",
1218
- },
1219
- {
1220
- name: "reasonDetail",
1221
- type: "string",
1222
- required: true,
1223
- description: "10–500 character operator reason; do not auto-fill placeholders",
1224
- },
1225
- {
1226
- name: "confirmationId",
1227
- type: "string",
1228
- required: true,
1229
- description: "human confirmation receipt id from prepareMutationGate",
1230
- },
1231
- ],
1232
- modelCallable: "prepare-only",
1233
- humanConfirmation: "required",
1234
- },
1235
1054
  {
1236
1055
  action: "prepareMutationGate",
1237
1056
  cli: "console prepare-mutation-gate",
@@ -1240,13 +1059,13 @@ export function buildOperatorCapabilitiesDocument() {
1240
1059
  resultSchemaVersion: 1,
1241
1060
  envelopeSchemaVersion: 1,
1242
1061
  requiredErrorCodes: ["INVALID_INPUT", "NOT_FOUND"],
1243
- description: "Prepare a one-shot Human Gate receipt for destructive reconcile, live DAG interrupt, and Night Scheduler mutations. Model may prepare; only the browser mutation gate may consume. Do not use for dagRerun / standaloneTaskRerun / workerTaskRetry.",
1062
+ description: "Prepare a one-shot Human Gate receipt for Night Scheduler mutations. Model may prepare; only the browser mutation gate may consume.",
1244
1063
  inputParams: [
1245
1064
  {
1246
1065
  name: "action",
1247
1066
  type: "string",
1248
1067
  required: true,
1249
- description: "target action: dagReconcileRun | dagRequestInterrupt | workerAdmissionPrepare | workerSchedulerAdd | workerSchedulerCancel | workerSchedulerHarvest | workerSchedulerDiscard. Not for standaloneTaskRerun / workerTaskRetry / dagRerun (those are autonomous).",
1068
+ description: "target action: workerAdmissionPrepare | workerSchedulerAdd | workerSchedulerCancel | workerSchedulerHarvest | workerSchedulerDiscard",
1250
1069
  },
1251
1070
  {
1252
1071
  name: "actionParams",
@@ -1498,7 +1317,7 @@ export function buildOperatorCapabilitiesDocument() {
1498
1317
  "INVALID_INPUT",
1499
1318
  "HUMAN_CONFIRMATION_REQUIRED",
1500
1319
  ],
1501
- description: "Full standalone task regenerate → validate → execute with parent lineage. Only when node rerun plan is ineligible or primaryRecovery recommends rerun-task; the server checks read-only run facts before accepting. Regeneration reuses the CURRENT contract snapshot: if the failure requires contract changes (e.g. allowing a catalog index path), first update the contract (re-import PRD, or adopt externally-edited sources via `task contract adopt`) BEFORE calling this action, otherwise the regenerated DAG repeats the same failure.",
1320
+ description: "Full standalone task regenerate → validate → execute with parent lineage. Only when node rerun plan is ineligible or primaryRecovery recommends rerun-task; the server checks read-only run facts before accepting.",
1502
1321
  inputParams: [
1503
1322
  {
1504
1323
  name: "runId",
@@ -1619,7 +1438,7 @@ export const OPERATOR_COMMAND_COVERAGE = Object.freeze([
1619
1438
  {
1620
1439
  command: "loop-agent task advance",
1621
1440
  coverage: "model-callable",
1622
- action: "taskAdvance",
1441
+ action: "newTask",
1623
1442
  source: "loop-agent",
1624
1443
  },
1625
1444
  {
@@ -2059,12 +1878,6 @@ export const OPERATOR_COMMAND_COVERAGE = Object.freeze([
2059
1878
  action: "dagReconcileRun",
2060
1879
  source: "loop-agent",
2061
1880
  },
2062
- {
2063
- command: "loop-agent dag request-interrupt",
2064
- coverage: "human-gated-required",
2065
- action: "dagRequestInterrupt",
2066
- source: "loop-agent",
2067
- },
2068
1881
  {
2069
1882
  command: "loop-agent dag rerun",
2070
1883
  coverage: "model-callable",
@@ -45,48 +45,6 @@ export function readPackageName(packageDir) {
45
45
  }
46
46
  return undefined;
47
47
  }
48
- /**
49
- * Read the build stamp (git SHA + build time) written by the build pipeline
50
- * into `<packageDir>/dist/build-stamp.json`. `undefined` when absent (e.g.
51
- * running from source via tsx, or a hand-assembled package), which callers
52
- * treat as "no stamp available".
53
- */
54
- export function readBuildStamp(packageDir) {
55
- try {
56
- const raw = readFileSync(path.join(packageDir, "dist", "build-stamp.json"), "utf-8");
57
- const parsed = JSON.parse(raw);
58
- if (parsed.schemaVersion !== 1 ||
59
- typeof parsed.version !== "string" ||
60
- typeof parsed.builtAt !== "string")
61
- return undefined;
62
- return parsed;
63
- }
64
- catch {
65
- return undefined;
66
- }
67
- }
68
- /**
69
- * Human-readable CLI version label: plain version, enriched with the build
70
- * stamp (short git SHA + build date) when available so a stale dist/ that was
71
- * built from an old commit is immediately recognizable in `--version`.
72
- */
73
- export function readCliVersionLabel(packageDir) {
74
- const version = readPackageVersion(packageDir) ?? "0.0.0";
75
- const stamp = readBuildStamp(packageDir);
76
- if (!stamp?.gitSha)
77
- return version;
78
- return `${version} (build ${stamp.gitSha.slice(0, 8)} ${stamp.builtAt.slice(0, 10)})`;
79
- }
80
- /**
81
- * Strip the build-stamp suffix from a CLI `--version` label, returning the bare
82
- * SemVer. Controller-identity/preflight comparisons must use this rather than
83
- * the raw label so a build-stamped `0.35.0 (build 7f8c5b5b …)` still matches
84
- * `packageVersion === "0.35.0"`.
85
- */
86
- export function normalizeCliVersionLabel(label) {
87
- const match = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/.exec(label.trim());
88
- return match ? match[0] : label.trim();
89
- }
90
48
  /**
91
49
  * Compute the SHA-256 hash of a file on disk.
92
50
  *
@@ -5,73 +5,7 @@
5
5
  */
6
6
  import { readFile, readdir, stat } from "node:fs/promises";
7
7
  import path from "node:path";
8
- import { z } from "zod";
9
- import { OPENSPEC_SPEC_DIRS, OPENSPEC_SPEC_EXT_RE, isOpenspecSpecFilePath, } from "../shared/openspec-spec.js";
10
- const classifiedPathSchema = z
11
- .string()
12
- .refine((candidate) => isOpenspecSpecFilePath(candidate), "classified path must be a repo-relative supported spec file under openspec/schemas/, openspec/project-specs/, or ai_workspace/");
13
- /**
14
- * Stable semantic partition of `designEvidence.normativePaths`. Keys are a
15
- * generic directory convention (not project-specific file names); the same
16
- * repository therefore yields the same classification on repeated probes.
17
- */
18
- export const frontendDesignClassifiedSchema = z
19
- .object({
20
- schemas: z.array(classifiedPathSchema),
21
- codeTemplate: z.array(classifiedPathSchema),
22
- rule: z
23
- .object({
24
- api: z.array(classifiedPathSchema),
25
- mock: z.array(classifiedPathSchema),
26
- router: z.array(classifiedPathSchema),
27
- hooks: z.array(classifiedPathSchema),
28
- utils: z.array(classifiedPathSchema),
29
- components: z.array(classifiedPathSchema),
30
- other: z.array(classifiedPathSchema),
31
- })
32
- .strict(),
33
- theme: z.array(classifiedPathSchema),
34
- component: z.array(classifiedPathSchema),
35
- uiOther: z.array(classifiedPathSchema),
36
- advisoryOther: z.array(classifiedPathSchema),
37
- })
38
- .strict();
39
- export const frontendDesignEvidenceSchema = z
40
- .object({
41
- normativePaths: z.array(z.string()),
42
- advisoryPaths: z.array(z.string()),
43
- conflicts: z.array(z.string()),
44
- classified: frontendDesignClassifiedSchema,
45
- })
46
- .strict()
47
- .superRefine((evidence, ctx) => {
48
- const classified = [
49
- ...evidence.classified.schemas,
50
- ...evidence.classified.codeTemplate,
51
- ...evidence.classified.rule.api,
52
- ...evidence.classified.rule.mock,
53
- ...evidence.classified.rule.router,
54
- ...evidence.classified.rule.hooks,
55
- ...evidence.classified.rule.utils,
56
- ...evidence.classified.rule.components,
57
- ...evidence.classified.rule.other,
58
- ...evidence.classified.theme,
59
- ...evidence.classified.component,
60
- ...evidence.classified.uiOther,
61
- ...evidence.classified.advisoryOther,
62
- ];
63
- const classifiedSet = new Set(classified);
64
- const normativeSet = new Set(evidence.normativePaths);
65
- const onlyClassified = [...classifiedSet].filter((candidate) => !normativeSet.has(candidate));
66
- const onlyNormative = [...normativeSet].filter((candidate) => !classifiedSet.has(candidate));
67
- if (onlyClassified.length > 0 || onlyNormative.length > 0) {
68
- ctx.addIssue({
69
- code: z.ZodIssueCode.custom,
70
- message: `classified must partition normativePaths exactly; extra in classified: ${onlyClassified.join(", ") || "(none)"}; missing from classified: ${onlyNormative.join(", ") || "(none)"}`,
71
- path: ["classified"],
72
- });
73
- }
74
- });
8
+ import { OPENSPEC_SPEC_DIRS, OPENSPEC_SPEC_EXT_RE } from "../shared/openspec-spec.js";
75
9
  async function exists(filePath) {
76
10
  try {
77
11
  await stat(filePath);
@@ -107,14 +41,13 @@ async function listOpenspec(repoRoot) {
107
41
  continue;
108
42
  await walk(root, 0);
109
43
  }
110
- out.sort();
111
44
  return out.slice(0, 40);
112
45
  async function walk(dir, depth) {
113
46
  if (depth > 4)
114
47
  return;
115
48
  let entries = [];
116
49
  try {
117
- entries = (await readdir(dir)).sort();
50
+ entries = await readdir(dir);
118
51
  }
119
52
  catch {
120
53
  return;
@@ -135,97 +68,6 @@ async function listOpenspec(repoRoot) {
135
68
  }
136
69
  }
137
70
  }
138
- function emptyClassified() {
139
- return {
140
- schemas: [],
141
- codeTemplate: [],
142
- rule: { api: [], mock: [], router: [], hooks: [], utils: [], components: [], other: [] },
143
- theme: [],
144
- component: [],
145
- uiOther: [],
146
- advisoryOther: [],
147
- };
148
- }
149
- /**
150
- * Classify discovered openspec paths by generic directory convention. Only
151
- * `isOpenspecSpecFilePath` paths enter a bucket; the classifier never invents
152
- * project-specific file names. Basename stems use word-boundary matching so
153
- * e.g. `*api*` (case-insensitive) maps to `rule.api` without hardcoding names.
154
- */
155
- function classifyOpenspecPaths(paths) {
156
- const classified = emptyClassified();
157
- const ruleStem = (basename) => {
158
- const lower = basename.toLowerCase();
159
- const stems = [
160
- ["api", /\bapi\b/],
161
- ["mock", /\bmock\b/],
162
- ["router", /\brouter\b/],
163
- ["hooks", /\bhooks?\b/],
164
- ["utils", /\butils?\b/],
165
- ["components", /\bcomponents?\b/],
166
- ];
167
- for (const [key, re] of stems) {
168
- if (re.test(lower))
169
- return key;
170
- }
171
- return "other";
172
- };
173
- for (const candidate of paths) {
174
- if (!isOpenspecSpecFilePath(candidate))
175
- continue;
176
- const segments = candidate.split("/");
177
- const basename = segments[segments.length - 1] ?? "";
178
- if (candidate.startsWith("openspec/schemas/")) {
179
- classified.schemas.push(candidate);
180
- }
181
- else if (candidate.startsWith("openspec/project-specs/templates/")) {
182
- classified.codeTemplate.push(candidate);
183
- }
184
- else if (candidate.startsWith("openspec/project-specs/rules/")) {
185
- classified.rule[ruleStem(basename)].push(candidate);
186
- }
187
- else if (candidate.startsWith("openspec/project-specs/ui/")) {
188
- const lower = basename.toLowerCase();
189
- if (/\btheme\b/.test(lower))
190
- classified.theme.push(candidate);
191
- else if (/\bcomponents?\b/.test(lower))
192
- classified.component.push(candidate);
193
- else
194
- classified.uiOther.push(candidate);
195
- }
196
- else {
197
- classified.advisoryOther.push(candidate);
198
- }
199
- }
200
- const sortBucket = (bucket) => bucket.sort();
201
- classified.schemas = sortBucket(classified.schemas);
202
- classified.codeTemplate = sortBucket(classified.codeTemplate);
203
- for (const key of [
204
- "api",
205
- "mock",
206
- "router",
207
- "hooks",
208
- "utils",
209
- "components",
210
- "other",
211
- ]) {
212
- classified.rule[key] = sortBucket(classified.rule[key]);
213
- }
214
- classified.theme = sortBucket(classified.theme);
215
- classified.component = sortBucket(classified.component);
216
- classified.uiOther = sortBucket(classified.uiOther);
217
- classified.advisoryOther = sortBucket(classified.advisoryOther);
218
- return classified;
219
- }
220
- function buildDesignEvidence(openspec) {
221
- const normativePaths = [...new Set(openspec)].sort();
222
- return frontendDesignEvidenceSchema.parse({
223
- normativePaths,
224
- advisoryPaths: [],
225
- conflicts: [],
226
- classified: classifyOpenspecPaths(normativePaths),
227
- });
228
- }
229
71
  export function buildAdapterGuidance(capability) {
230
72
  const lines = [
231
73
  "## Frontend project capability (generation-time)",
@@ -279,7 +121,11 @@ export async function discoverFrontendProjectCapability(repoRoot) {
279
121
  a11y: { status: "unknown", tools: [], evidencePaths: [] },
280
122
  evidencePaths: openspec.slice(0, 5),
281
123
  reasons: ["package.json missing or unreadable"],
282
- designEvidence: buildDesignEvidence(openspec),
124
+ designEvidence: {
125
+ normativePaths: openspec,
126
+ advisoryPaths: [],
127
+ conflicts: [],
128
+ },
283
129
  };
284
130
  if (openspec.length) {
285
131
  base.reasons.push(`openspec spec files discovered: ${openspec.slice(0, 5).join(", ")}`);
@@ -438,7 +284,11 @@ export async function discoverFrontendProjectCapability(repoRoot) {
438
284
  if (hasDep(deps, "vue-router"))
439
285
  router = "vue-router";
440
286
  const openspec = await listOpenspec(repoRoot);
441
- const designEvidence = buildDesignEvidence(openspec);
287
+ const designEvidence = {
288
+ normativePaths: openspec,
289
+ advisoryPaths: [],
290
+ conflicts: [],
291
+ };
442
292
  if (openspec.length)
443
293
  evidencePaths.push(...openspec.slice(0, 5));
444
294
  const base = {
@@ -2,13 +2,6 @@ const FRESH_SOURCE_MISSING_CODES = new Set([
2
2
  "MISSING_REQUIREMENT",
3
3
  "MISSING_CONSTRAINTS",
4
4
  ]);
5
- function hasKnowledgeSyncFeatureSurface(featureId, allowedPaths) {
6
- const featureRoot = `features/${featureId}/`;
7
- return allowedPaths.some((pathGlob) => {
8
- const normalized = pathGlob.replaceAll("\\", "/").replace(/^\.\//, "");
9
- return normalized === `features/${featureId}` || normalized.startsWith(featureRoot);
10
- });
11
- }
12
5
  export function isFreshLegacyUnversioned(state, sourceFilesPresent) {
13
6
  if (state.effectiveStatus !== "legacy-unversioned")
14
7
  return false;
@@ -112,16 +105,6 @@ export function listPrepareGaps(input) {
112
105
  message: "featureId is required when taskKind is knowledge-sync",
113
106
  });
114
107
  }
115
- if (draft.taskKind === "knowledge-sync" &&
116
- draft.featureId &&
117
- !hasKnowledgeSyncFeatureSurface(draft.featureId, draft.constraints.allowedPaths)) {
118
- gaps.push({
119
- code: "KNOWLEDGE_SYNC_PATH_MISMATCH",
120
- level: "blocking",
121
- field: "allowedPaths",
122
- message: `knowledge-sync for ${draft.featureId} requires an allowed path under features/${draft.featureId}/; ordinary docs-only work should use taskKind standard`,
123
- });
124
- }
125
108
  if (draft.requirement.scope.length === 0) {
126
109
  gaps.push({
127
110
  code: "EMPTY_SCOPE",
@@ -15,21 +15,7 @@ const HEADING_ALIASES = {
15
15
  "已确认需求",
16
16
  ]),
17
17
  nonGoals: new Set(["非目标", "out of scope", "non-goals", "non goals"]),
18
- acceptance: new Set([
19
- "验收标准",
20
- "acceptance criteria",
21
- "acceptance",
22
- // Common zh-CN product-writing variants seen in real PRDs; without
23
- // these a PRD re-import flips to deterministic parse and loses all AC
24
- // (EMPTY_ACCEPTANCE deadlock on the pendingPrdRefresh path).
25
- "完成定义",
26
- "完成标准",
27
- "验收",
28
- "验收条件",
29
- "验收准则",
30
- "dod",
31
- "definition of done",
32
- ]),
18
+ acceptance: new Set(["验收标准", "acceptance criteria", "acceptance"]),
33
19
  constraints: new Set(["约束", "constraints", "不变式", "invariants"]),
34
20
  openQuestions: new Set(["待决问题", "open questions", "open-questions"]),
35
21
  assumptions: new Set(["假设", "assumptions"]),