@fitlab-ai/agent-infra 0.11.6 → 0.11.7

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/dist/lib/git/primary-worktree.js +58 -0
  2. package/dist/lib/platform/pull-requests.js +29 -3
  3. package/dist/lib/sandbox/control/server.js +6 -2
  4. package/dist/lib/server/config.js +7 -27
  5. package/dist/lib/server/daemon.js +4 -3
  6. package/dist/lib/server/process-control.js +4 -2
  7. package/dist/lib/server/runner.js +4 -3
  8. package/dist/lib/task/artifact-lifecycle.js +42 -10
  9. package/dist/lib/task/artifact-receipts.js +62 -22
  10. package/dist/lib/task/capabilities.js +22 -4
  11. package/dist/lib/task/completion-facts.js +56 -0
  12. package/dist/lib/task/create-service.js +6 -2
  13. package/dist/lib/task/create.js +46 -82
  14. package/dist/lib/task/events.js +268 -195
  15. package/dist/lib/task/finalization-handoff.js +2 -1
  16. package/dist/lib/task/invalidation-command.js +4 -2
  17. package/dist/lib/task/invalidation.js +38 -9
  18. package/dist/lib/task/local-artifact-finalization.js +17 -3
  19. package/dist/lib/task/qualification-audit.js +23 -105
  20. package/dist/lib/task/review-finalization.js +7 -0
  21. package/dist/lib/task/review-identity.js +69 -0
  22. package/dist/lib/task/sections.js +6 -5
  23. package/dist/lib/task/task-execution-lock.js +6 -3
  24. package/dist/lib/task/verification-engine.js +14 -63
  25. package/dist/lib/task/verification.js +35 -38
  26. package/dist/lifecycle-build-manifest.json +3 -3
  27. package/lib/git/primary-worktree.ts +62 -0
  28. package/lib/platform/pull-requests.ts +27 -3
  29. package/lib/sandbox/control/server.ts +5 -2
  30. package/lib/server/config.ts +7 -29
  31. package/lib/server/daemon.ts +5 -3
  32. package/lib/server/process-control.ts +4 -2
  33. package/lib/server/runner.ts +7 -4
  34. package/lib/server/server.schema.json +0 -4
  35. package/lib/task/artifact-lifecycle.ts +35 -11
  36. package/lib/task/artifact-receipts.ts +55 -15
  37. package/lib/task/capabilities.ts +17 -3
  38. package/lib/task/completion-facts.ts +66 -0
  39. package/lib/task/create-service.ts +7 -3
  40. package/lib/task/create.ts +44 -71
  41. package/lib/task/events.ts +248 -182
  42. package/lib/task/finalization-handoff.ts +2 -1
  43. package/lib/task/invalidation-command.ts +4 -2
  44. package/lib/task/invalidation.ts +35 -10
  45. package/lib/task/local-artifact-finalization.ts +13 -3
  46. package/lib/task/qualification-audit.ts +21 -114
  47. package/lib/task/review-finalization.ts +7 -0
  48. package/lib/task/review-identity.ts +85 -0
  49. package/lib/task/sections.ts +6 -6
  50. package/lib/task/task-execution-lock.ts +7 -3
  51. package/lib/task/verification-engine.ts +13 -86
  52. package/lib/task/verification.ts +34 -38
  53. package/package.json +1 -1
  54. package/templates/.agents/rules/README.en.md +1 -0
  55. package/templates/.agents/rules/README.zh-CN.md +1 -0
  56. package/templates/.agents/rules/decision-qualification.en.md +4 -3
  57. package/templates/.agents/rules/decision-qualification.zh-CN.md +4 -3
  58. package/templates/.agents/rules/validation-output.en.md +19 -0
  59. package/templates/.agents/rules/validation-output.zh-CN.md +19 -0
  60. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
  61. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  62. package/templates/.agents/skills/block-task/SKILL.en.md +2 -0
  63. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
  64. package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -0
  65. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
  66. package/templates/.agents/skills/code-task/SKILL.en.md +3 -1
  67. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
  68. package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -8
  69. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -8
  70. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  71. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  72. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  73. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  74. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -0
  75. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -0
  76. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
  77. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  78. package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
  79. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  80. package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -0
  81. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  82. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -0
  83. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  84. package/templates/.agents/skills/import-issue/SKILL.en.md +2 -0
  85. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  86. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
  87. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  88. package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -1
  89. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -1
  90. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -8
  91. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -8
  92. package/templates/.agents/skills/review-code/SKILL.en.md +13 -12
  93. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -12
  94. package/templates/.agents/skills/review-code/config/verify.en.json +1 -3
  95. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -3
  96. package/templates/.agents/skills/review-code/reference/output-templates.en.md +23 -46
  97. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +27 -48
  98. package/templates/.agents/skills/review-code/reference/report-template.en.md +8 -11
  99. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +8 -11
  100. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -2
  101. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -2
  102. package/templates/.agents/skills/review-plan/SKILL.en.md +3 -1
  103. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -1
  104. package/templates/.agents/skills/review-plan/reference/report-template.en.md +5 -9
  105. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +5 -9
  106. package/templates/.agents/skills/review-pr/SKILL.en.md +2 -0
  107. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +2 -0
  108. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
  109. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
  110. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  111. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  112. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -0
  113. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
  114. package/templates/.agents/templates/task.en.md +2 -2
  115. package/templates/.agents/templates/task.zh-CN.md +2 -2
@@ -4,14 +4,13 @@ import path from "node:path";
4
4
  import process from "node:process";
5
5
  import { execFileSync } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
7
- import { extractReviewBaseline, extractReviewTargetHead, extractReviewedHead, extractReviewDiffBase, extractReviewDiffFingerprint, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact, loadPostReviewConfig, parseReviewVerdict, resolvePostReviewGlobs } from "./verification-review.js";
7
+ import { extractReviewTargetHead, extractReviewedHead, extractReviewDiffBase, findAuthoritativeReviewCodeArtifact, loadPostReviewConfig, parseReviewVerdict, resolvePostReviewGlobs } from "./verification-review.js";
8
8
  import { check as checkPlatformSync } from "../platform/verification-sync.js";
9
9
  import { check as checkRequiredChecks } from "../platform/verification-required.js";
10
10
  import { inspectPlatformPullRequest } from "../platform/pull-requests.js";
11
11
  import { resolveMaterializedReviewedHeadRelation } from "../platform/change-request-git-evidence.js";
12
12
  import { resolveReviewedHeadRelation } from "../platform/merged-pr-equivalence.js";
13
13
  import { resolveLocalReviewedCommitRelation } from "../git/reviewed-commit-equivalence.js";
14
- import { resolveBranchWorktree } from "../git/branch-worktree.js";
15
14
  import { parseTypedTaskFrontmatter } from "./frontmatter.js";
16
15
  import { LEDGER_TERMINAL, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from "./ledger.js";
17
16
  import { isValidAgentInfraVersion } from "../version.js";
@@ -20,11 +19,9 @@ import { inspectDecisionDetailDuplicates } from "./decision-details.js";
20
19
  import { parseImplementationInputs } from "./implementation-inputs.js";
21
20
  import { scanVisibleMarkdown } from "./markdown.js";
22
21
  import { loadVerificationConfig } from "./verification-config.js";
23
- import { snapshotReview } from "../git/review-snapshot.js";
24
22
  import { inspectActivityLog } from "./activity-log.js";
25
23
  import { parseWorkflowWarnings } from "./workflow-warnings.js";
26
24
  import { OrchestrationStateError, readRun } from "./orchestration.js";
27
- import { resolveDeliveryTarget } from "./delivery-target.js";
28
25
  import { readPrDeliveryFact } from "./pr-delivery-fact.js";
29
26
  import { validateLocalArtifact } from "./local-artifact-finalization.js";
30
27
  import { getArtifactSchema } from "./artifact-schema.js";
@@ -35,6 +32,8 @@ import { readManualValidationCompletion } from "./manual-validation-completion.j
35
32
  import { sha256File } from "./artifact-receipts.js";
36
33
  import { summaryCommentState } from "../platform/pr-summary.js";
37
34
  import { taskIssueIdentity } from "../platform/task-identities.js";
35
+ import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.js';
36
+ import { inspectReviewIdentity } from './review-identity.js';
38
37
  const TASK_ENUMS = {
39
38
  type: ["feature", "bugfix", "refactor", "docs", "chore"],
40
39
  workflow: ["feature-development", "bug-fix", "refactoring"],
@@ -1014,12 +1013,16 @@ function checkReviewFact({ taskDir, artifactFile, repositoryRoot }) {
1014
1013
  return failResult("review-fact", task.message);
1015
1014
  }
1016
1015
  const content = fs.readFileSync(resolvedArtifact.path, "utf8");
1016
+ if (!hasArtifactCompletionFact(task.content, resolvedArtifact.path, 'review-code.completed')) {
1017
+ return failResult("review-fact", `Review Code completion fact does not match ${path.basename(resolvedArtifact.path)}`);
1018
+ }
1019
+ if (!hasArtifactCompletionLog(task.content, path.basename(resolvedArtifact.path), 'review-code.completed')) {
1020
+ return failResult("review-fact", `Review Code completion entry does not reference ${path.basename(resolvedArtifact.path)}`);
1021
+ }
1017
1022
  const verdict = parseReviewVerdict(content);
1018
1023
  const reviewTargetHead = extractReviewTargetHead(content);
1019
1024
  const reviewReviewedHead = extractReviewedHead(content);
1020
1025
  const reviewDiffBase = extractReviewDiffBase(content);
1021
- const reviewedFingerprint = extractReviewDiffFingerprint(content);
1022
- const reviewedTree = extractReviewedSnapshotTree(content);
1023
1026
  if (!["通过", "需要修改", "拒绝", "Approved", "Changes Requested", "Rejected"].includes(verdict)) {
1024
1027
  return failResult("review-fact", `Unsupported review verdict '${verdict}'`);
1025
1028
  }
@@ -1031,71 +1034,19 @@ function checkReviewFact({ taskDir, artifactFile, repositoryRoot }) {
1031
1034
  if (!reviewTargetHead || !reviewReviewedHead || !reviewDiffBase) {
1032
1035
  return failResult("review-fact", "Review fact must record target head, reviewed head, and diff base");
1033
1036
  }
1034
- let gitRoot;
1035
- let head;
1036
- let baseline;
1037
- let targetHead;
1038
- let diffBase;
1039
- try {
1040
- const taskRepositoryRoot = execFileSync("git", ["-C", taskDir, "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
1041
- gitRoot = reviewWorktreeForTask(taskRepositoryRoot, String(task.metadata.branch || "").trim()) ?? taskRepositoryRoot;
1042
- head = execFileSync("git", ["-C", gitRoot, "rev-parse", "HEAD"], { encoding: "utf8" }).trim();
1043
- const target = resolveDeliveryTarget(gitRoot, { remote: deliveryRemote, baseRef: deliveryBaseRef });
1044
- if (!target.ok)
1045
- throw new Error(target.message);
1046
- const reviewedRef = reviewReviewedHead;
1047
- baseline = execFileSync("git", ["-C", gitRoot, "rev-parse", `${reviewedRef}^{commit}`], { encoding: "utf8" }).trim();
1048
- targetHead = execFileSync("git", ["-C", gitRoot, "rev-parse", `${reviewTargetHead}^{commit}`], { encoding: "utf8" }).trim();
1049
- const computed = execFileSync("git", ["-C", gitRoot, "merge-base", baseline, targetHead], { encoding: "utf8" }).trim();
1050
- diffBase = execFileSync("git", ["-C", gitRoot, "rev-parse", `${reviewDiffBase}^{commit}`], { encoding: "utf8" }).trim();
1051
- if (diffBase !== computed)
1052
- throw new Error('saved review diff base does not match merge-base(reviewed head, target head)');
1053
- }
1054
- catch {
1055
- return blockedResult("review-fact", `Unable to resolve saved review commits in the task repository; re-run review-code`);
1056
- }
1057
- if (baseline !== head) {
1058
- return failResult("review-fact", `Reviewed head ${baseline.slice(0, 8)} does not match current HEAD ${head.slice(0, 8)}; re-run review-code`);
1059
- }
1060
- let actualSnapshot;
1061
- try {
1062
- actualSnapshot = snapshotReview({
1063
- cwd: gitRoot,
1064
- mode: "worktree",
1065
- baseline,
1066
- diffBase,
1067
- globs: resolvePostReviewGlobs({}, loadPostReviewConfig(repositoryRoot))
1068
- });
1069
- }
1070
- catch {
1071
- return blockedResult("review-fact", `Unable to recompute reviewed diff fingerprint from baseline ${baseline.slice(0, 8)}; re-run review-code`);
1072
- }
1073
- if (actualSnapshot.fingerprint !== reviewedFingerprint) {
1074
- return failResult("review-fact", `Reviewed diff fingerprint does not match the current worktree for baseline ${baseline.slice(0, 8)}; re-run review-code`);
1075
- }
1076
- if (!reviewedTree || actualSnapshot.tree !== reviewedTree) {
1077
- return failResult("review-fact", `Reviewed snapshot tree does not match the current worktree for baseline ${baseline.slice(0, 8)}; re-run review-code`);
1037
+ const identity = inspectReviewIdentity(taskDir, content, repositoryRoot);
1038
+ if (identity.status !== 'matched' || !identity.reviewedHead) {
1039
+ return failResult('review-fact', `Review identity is ${identity.status}: ${identity.message}; re-run review-code`);
1078
1040
  }
1041
+ const baseline = identity.reviewedHead;
1079
1042
  if (["通过", "Approved"].includes(verdict)) {
1080
1043
  const lastReviewedCommit = String(task.metadata.last_reviewed_commit || "").trim();
1081
- const cleanSnapshot = actualSnapshot.tree === execFileSync("git", ["-C", gitRoot, "rev-parse", `${baseline}^{tree}`], { encoding: "utf8" }).trim();
1082
- if (cleanSnapshot && lastReviewedCommit !== baseline) {
1044
+ if (lastReviewedCommit !== baseline) {
1083
1045
  return failResult("review-fact", `Approved clean review must set task last_reviewed_commit to reviewed head ${baseline.slice(0, 8)}`);
1084
1046
  }
1085
- if (!cleanSnapshot && lastReviewedCommit) {
1086
- return failResult("review-fact", "Approved review with uncommitted changes must remain unanchored until commit");
1087
- }
1088
1047
  }
1089
1048
  return passResult("review-fact", `Review fact valid for ${path.basename(resolvedArtifact.path)} at ${baseline.slice(0, 8)}`);
1090
1049
  }
1091
- function reviewWorktreeForTask(repositoryRoot, branch) {
1092
- if (!branch)
1093
- return null;
1094
- const worktree = resolveBranchWorktree(repositoryRoot, branch);
1095
- if (worktree)
1096
- return worktree;
1097
- throw new Error(`Task branch '${branch}' is not checked out in a registered worktree`);
1098
- }
1099
1050
  function resolvePostReviewBaseline({ gitRoot, lastReviewedCommit, reviewArtifact }) {
1100
1051
  if (SHA_PATTERN.test(lastReviewedCommit) && gitCommitExists(gitRoot, lastReviewedCommit)) {
1101
1052
  return { ok: true, sha: lastReviewedCommit };
@@ -89,47 +89,44 @@ async function verifyTaskEvent(request, options = {}) {
89
89
  function statusLabel(status) {
90
90
  return status === 'fail' ? 'FAIL' : status === 'blocked' ? 'BLOCKED' : 'pass';
91
91
  }
92
- function renderPayload(payload) {
93
- const lines = [];
94
- if (Array.isArray(payload.checks)) {
95
- const gateMode = typeof payload.gate === 'string';
96
- lines.push(gateMode
97
- ? `Verification: ${payload.gate} | Skill: ${payload.skill}`
98
- : `Check: ${payload.status} | Skill: ${payload.skill} | Type: ${payload.type}`, '');
99
- for (const check of payload.checks) {
100
- const raw = statusLabel(check.status);
101
- const effective = statusLabel(check.effectiveStatus ?? check.status);
102
- const reason = check.reason ?? 'CHECK_FAILED';
103
- const message = check.message ?? reason;
104
- lines.push(` [${effective}] ${check.checkId ?? check.type} (${check.classification ?? 'hard'}; raw ${raw}; reason ${reason}) - ${message}; ${check.action ?? 'Review validation output'}`);
105
- }
106
- const summary = payload.summary ?? (() => {
107
- const checks = payload.checks;
108
- const statuses = checks.map((check) => check.effectiveStatus ?? check.status);
109
- return `${statuses.filter((status) => status === 'pass').length} passed, ${statuses.filter((status) => status === 'fail').length} failed${statuses.includes('blocked') ? `, ${statuses.filter((status) => status === 'blocked').length} blocked` : ''}`;
110
- })();
111
- lines.push('', `Result: ${summary} - ${payload.action}`);
112
- }
113
- else {
114
- lines.push(`Check: ${payload.status} | Skill: ${payload.skill} | Type: ${payload.type}`, '');
115
- const raw = statusLabel(payload.status);
116
- const effective = statusLabel(payload.effectiveStatus ?? payload.status);
117
- const reason = payload.reason ?? 'CHECK_FAILED';
118
- lines.push(` [${effective}] ${payload.checkId ?? payload.type} (${payload.classification ?? 'hard'}; raw ${raw}; reason ${reason}) - ${payload.message ?? reason}; ${payload.action ?? 'Review validation output'}`);
119
- const status = payload.effectiveStatus ?? payload.status;
120
- const summary = status === 'pass' ? '1 passed, 0 failed'
121
- : status === 'blocked' ? '0 passed, 0 failed, 1 blocked' : '0 passed, 1 failed';
122
- const action = status === 'pass' ? 'Requested check passed'
123
- : status === 'blocked' ? `Resolve blocked ${payload.type} check and re-run check`
124
- : `Fix ${payload.type} issues and re-run check`;
125
- lines.push('', `Result: ${summary} - ${action}`);
126
- }
127
- return `${lines.join('\n')}\n`;
128
- }
129
92
  function renderTaskVerification(result) {
130
93
  if (result.status === 'failed')
131
94
  return `Verification failed: ${result.error?.code ?? 'VERIFY_FAILED'} - ${result.error?.message ?? 'unknown error'}\n`;
132
- return result.invocations.map((invocation) => renderPayload(invocation.payload)).join('');
95
+ const target = `${result.event}${result.artifact ? ` (${result.artifact})` : ''}`;
96
+ const lines = [`Verification: ${result.status} | Target: ${target} | Skill: ${result.skill}`];
97
+ const checks = result.invocations.flatMap(({ payload }) => {
98
+ if (Array.isArray(payload.checks))
99
+ return payload.checks;
100
+ return [payload];
101
+ });
102
+ const counts = {
103
+ pass: checks.filter((check) => (check.effectiveStatus ?? check.status) === 'pass').length,
104
+ fail: checks.filter((check) => (check.effectiveStatus ?? check.status) === 'fail').length,
105
+ blocked: checks.filter((check) => (check.effectiveStatus ?? check.status) === 'blocked').length
106
+ };
107
+ const summary = `${counts.pass} passed, ${counts.fail} failed${counts.blocked > 0 ? `, ${counts.blocked} blocked` : ''}`;
108
+ lines.push('', `Result: ${summary}`);
109
+ for (const check of checks) {
110
+ const rawStatus = check.status;
111
+ const effectiveStatus = check.effectiveStatus ?? rawStatus;
112
+ const isSoftWarning = (rawStatus === 'fail' || rawStatus === 'blocked') && effectiveStatus === 'pass';
113
+ const reason = check.reason ?? 'CHECK_FAILED';
114
+ const message = check.message ?? reason;
115
+ const action = check.action ?? 'Review validation output';
116
+ const id = check.checkId ?? check.type;
117
+ const isHumanExemptionNotice = id === 'post-review-commit'
118
+ && typeof message === 'string'
119
+ && /\bhuman-decided (?:post-review )?exemption\b/i.test(message)
120
+ && /\bPRC-\d+\s*:/.test(message);
121
+ if (effectiveStatus === 'pass' && !isSoftWarning && !isHumanExemptionNotice)
122
+ continue;
123
+ lines.push(isSoftWarning
124
+ ? `Warning: ${id} (${rawStatus === 'blocked' ? `raw BLOCKED; ` : ''}${reason}) - ${message}; ${action}`
125
+ : isHumanExemptionNotice
126
+ ? `Notice: ${id} - ${message}; ${action}`
127
+ : ` [${statusLabel(effectiveStatus)}] ${id} (${reason}) - ${message}; ${action}`);
128
+ }
129
+ return `${lines.join('\n')}\n`;
133
130
  }
134
131
  export { VERIFICATION_CATALOG, renderTaskVerification, verifyTaskEvent };
135
132
  //# sourceMappingURL=verification.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "protocolVersion": 3,
3
- "packageVersion": "0.11.6",
4
- "sourceInputHash": "402048e73650c5ac4edfb1bc4b48ca3fceb7b9068790b1321281c4994121e80c",
5
- "internalExecutableBuildHash": "47ecd04ac30183443b2b86f83d1668b8bdf9e59162cc9e2cedd333d3011843e9"
3
+ "packageVersion": "0.11.7",
4
+ "sourceInputHash": "095f0cf30aa29fcd2dd45123dc755e5097d7a9f81de33420461fd943cff122b2",
5
+ "internalExecutableBuildHash": "a956f2ed366e025a111a87bc1f542b87d83d4025e068304f95b1aae704709d3f"
6
6
  }
@@ -0,0 +1,62 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+
4
+ import { sameFilesystemEntry } from './worktree-identity.ts';
5
+
6
+ function gitText(repositoryRoot: string, args: string[]): string {
7
+ return execFileSync('git', ['-C', repositoryRoot, ...args], {
8
+ encoding: 'utf8',
9
+ stdio: ['ignore', 'pipe', 'pipe']
10
+ }).replace(/\r?\n$/, '');
11
+ }
12
+
13
+ function isPrimaryWorktree(candidate: string, expectedCommonDirectory: string): boolean {
14
+ try {
15
+ const root = fs.realpathSync.native(candidate);
16
+ if (!fs.statSync(root).isDirectory()) return false;
17
+ if (gitText(root, ['rev-parse', '--show-prefix']) !== '') return false;
18
+
19
+ const commonDirectory = gitText(root, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
20
+ const gitDirectory = gitText(root, ['rev-parse', '--absolute-git-dir']);
21
+ return sameFilesystemEntry(commonDirectory, expectedCommonDirectory) &&
22
+ sameFilesystemEntry(gitDirectory, expectedCommonDirectory);
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ function worktreePaths(repositoryRoot: string): string[] {
29
+ const output = execFileSync('git', ['-C', repositoryRoot, 'worktree', 'list', '--porcelain', '-z'], {
30
+ encoding: 'utf8',
31
+ stdio: ['ignore', 'pipe', 'pipe']
32
+ });
33
+
34
+ return output
35
+ .split('\0\0')
36
+ .flatMap((record) => record.split('\0'))
37
+ .filter((field) => field.startsWith('worktree '))
38
+ .map((field) => field.slice('worktree '.length));
39
+ }
40
+
41
+ export function resolvePrimaryWorktreeRoot(repositoryRoot: string): string {
42
+ try {
43
+ const root = fs.realpathSync.native(repositoryRoot);
44
+ if (!fs.statSync(root).isDirectory() || gitText(root, ['rev-parse', '--show-prefix']) !== '') {
45
+ throw new Error('repository root is not a worktree root');
46
+ }
47
+
48
+ const commonDirectory = gitText(root, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
49
+ const gitDirectory = gitText(root, ['rev-parse', '--absolute-git-dir']);
50
+ if (sameFilesystemEntry(gitDirectory, commonDirectory)) return root;
51
+
52
+ const matches = worktreePaths(root).filter((candidate) => isPrimaryWorktree(candidate, commonDirectory));
53
+ if (matches.length !== 1) {
54
+ throw new Error(matches.length === 0 ? 'primary worktree metadata was not found' : 'primary worktree metadata is ambiguous');
55
+ }
56
+
57
+ return fs.realpathSync.native(matches[0]!);
58
+ } catch (error) {
59
+ const detail = error instanceof Error ? error.message : String(error);
60
+ throw new Error(`server: unable to locate primary worktree: ${detail}`);
61
+ }
62
+ }
@@ -1140,9 +1140,33 @@ async function createExternalPullRequest(
1140
1140
  }
1141
1141
  if (base.prIdentity) {
1142
1142
  const inspected = await inspectExternalPullRequest(base, base.prIdentity!);
1143
- return inspected.status === 'failed' || inspected.status === 'blocked'
1144
- ? withCreation(inspected, PRECONDITION_NOT_CREATED)
1145
- : withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
1143
+ if (inspected.status === 'failed' || inspected.status === 'blocked') {
1144
+ return withCreation(inspected, PRECONDITION_NOT_CREATED);
1145
+ }
1146
+ const deliveredHeadSha = typeof base.frontmatter.delivery_remote_head === 'string'
1147
+ ? base.frontmatter.delivery_remote_head
1148
+ : undefined;
1149
+ const expectedHeadSha = verifiedHeadSha ?? deliveredHeadSha;
1150
+ if (!expectedHeadSha) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1151
+ platform: base.context.platform,
1152
+ capabilities: base.context.capabilities,
1153
+ resource: { kind: 'pull-request', number: base.prNumber, identity: base.prIdentity },
1154
+ pullRequest: inspected.pullRequest,
1155
+ error: { code: 'PR_HEAD_UNVERIFIED', message: 'Unable to verify the bound pull request head SHA', retryable: false }
1156
+ }), PRECONDITION_NOT_CREATED);
1157
+ const identity = validateWriterIdentity(base, inspected.pullRequest ?? null, {
1158
+ expectedHead: options.head,
1159
+ expectedHeadSha,
1160
+ errorCode: 'PR_BIND_IDENTITY_MISMATCH'
1161
+ });
1162
+ if (!identity.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1163
+ platform: base.context.platform,
1164
+ capabilities: base.context.capabilities,
1165
+ resource: { kind: 'pull-request', number: base.prNumber, identity: base.prIdentity },
1166
+ pullRequest: inspected.pullRequest,
1167
+ error: identity.error
1168
+ }), PRECONDITION_NOT_CREATED);
1169
+ return withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
1146
1170
  }
1147
1171
  if (options.dryRun) return withCreation(result('planned', base.resolved.taskId, base.issueNumber, null, {
1148
1172
  platform: base.context.platform,
@@ -272,12 +272,15 @@ function taskCreateDomainEvidence(
272
272
  ? output.task as Record<string, unknown> : null;
273
273
  const taskId = typeof task?.id === 'string' ? task.id : null;
274
274
  const shortId = typeof task?.shortId === 'string' ? task.shortId : null;
275
- if (!taskId || !shortId) return { consistent: false };
275
+ const state = task?.state;
276
+ if (!taskId || !['active', 'blocked', 'completed', 'archive'].includes(String(state))) return { consistent: false };
277
+ if (state === 'active' ? !shortId : output.status !== 'no-op' || shortId !== null) return { consistent: false };
276
278
  try {
277
279
  const resolved = resolveTaskRef(taskId, { repoRoot: manifest.repoRoot });
278
280
  const shortIds = loadShortIdByTaskId(manifest.repoRoot);
279
281
  return {
280
- consistent: resolved.ok && resolved.taskId === taskId && resolved.state === 'active' && shortIds.get(taskId) === shortId,
282
+ consistent: resolved.ok && resolved.taskId === taskId && resolved.state === state
283
+ && (state === 'active' ? shortIds.get(taskId) === shortId : !shortIds.has(taskId)),
281
284
  taskId,
282
285
  shortId
283
286
  };
@@ -1,8 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { homedir } from 'node:os';
4
- import { createHash } from 'node:crypto';
5
4
  import { execFileSync } from 'node:child_process';
5
+ import { resolvePrimaryWorktreeRoot } from '../git/primary-worktree.ts';
6
6
 
7
7
  export type ServerLogConfig = {
8
8
  path: string;
@@ -52,17 +52,7 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
52
52
  return typeof value === 'object' && value !== null && !Array.isArray(value);
53
53
  }
54
54
 
55
- // Daemon runtime state (log + PID) lives OUTSIDE the repo, under the user's home
56
- // directory, keyed by the .airc.json "project" AND a stable hash of the repo
57
- // root path:
58
- // ~/.agent-infra/logs/<project>/<repo-hash>/server.log
59
- // ~/.agent-infra/run/<project>/<repo-hash>/server.pid
60
- // The <project> segment groups a project's checkouts for readability; the
61
- // <repo-hash> segment guarantees that two checkouts/worktrees of the same
62
- // project (same "project" but different absolute path) get ISOLATED runtime
63
- // dirs, so they never read/control each other's daemon. Using os.homedir() +
64
- // path.join keeps this correct on Windows too (C:\Users\<name>\.agent-infra\...).
65
- // An explicit log.path in server.json/.local/env still overrides the log default.
55
+ // Runtime state is shared by all worktrees through the primary checkout.
66
56
  function resolveProjectKey(repoRoot: string): string {
67
57
  try {
68
58
  const airc = JSON.parse(
@@ -77,13 +67,8 @@ function resolveProjectKey(repoRoot: string): string {
77
67
  return path.basename(repoRoot);
78
68
  }
79
69
 
80
- // Short, stable, filesystem-safe discriminator for a checkout's absolute path.
81
- function repoKey(repoRoot: string): string {
82
- return createHash('sha256').update(repoRoot).digest('hex').slice(0, 12);
83
- }
84
-
85
- function runtimePath(repoRoot: string, projectKey: string, kind: 'logs' | 'run', file: string): string {
86
- return path.join(homedir(), '.agent-infra', kind, projectKey, repoKey(repoRoot), file);
70
+ function runtimePath(projectKey: string, kind: 'logs' | 'run', file: string): string {
71
+ return path.join(homedir(), '.agent-infra', kind, projectKey, file);
87
72
  }
88
73
 
89
74
  function detectRepoRoot(): string {
@@ -185,7 +170,7 @@ export function validateServerConfig(committed: Record<string, unknown>): Server
185
170
  }
186
171
 
187
172
  export function loadServerConfig({ rootDir }: { rootDir?: string } = {}): ServerConfig {
188
- const repoRoot = rootDir ?? detectRepoRoot();
173
+ const repoRoot = resolvePrimaryWorktreeRoot(rootDir ?? detectRepoRoot());
189
174
  const agentsDir = path.join(repoRoot, '.agents');
190
175
 
191
176
  const committed = readJsonIfPresent(path.join(agentsDir, 'server.json'));
@@ -205,18 +190,11 @@ export function loadServerConfig({ rootDir }: { rootDir?: string } = {}): Server
205
190
  const projectKey = resolveProjectKey(repoRoot);
206
191
 
207
192
  const log = isPlainObject(merged.log) ? merged.log : {};
208
- // No explicit log.path → default under ~/.agent-infra/logs/<project>/.
209
- // Explicit relative path resolves against the repo root; absolute is used as-is.
210
- const explicitPath = typeof log.path === 'string' ? log.path : null;
211
- const resolvedLogPath = explicitPath === null
212
- ? runtimePath(repoRoot, projectKey, 'logs', 'server.log')
213
- : (path.isAbsolute(explicitPath) ? explicitPath : path.join(repoRoot, explicitPath));
214
-
215
193
  return {
216
194
  repoRoot,
217
- pidFile: runtimePath(repoRoot, projectKey, 'run', 'server.pid'),
195
+ pidFile: runtimePath(projectKey, 'run', 'server.pid'),
218
196
  log: {
219
- path: resolvedLogPath,
197
+ path: runtimePath(projectKey, 'logs', 'server.log'),
220
198
  rotateAtBytes: typeof log.rotateAtBytes === 'number' ? log.rotateAtBytes : DEFAULT_ROTATE_BYTES
221
199
  },
222
200
  heartbeatMs: typeof merged.heartbeatMs === 'number' ? merged.heartbeatMs : DEFAULT_SERVER_CONFIG.heartbeatMs,
@@ -83,7 +83,7 @@ export function createMessageDispatcher(options: MessageDispatcherOptions): (mes
83
83
  chunkChars: typeof options.config.stream?.chunkChars === 'number' ? options.config.stream.chunkChars : 4000,
84
84
  throttleMs: typeof options.config.stream?.throttleMs === 'number' ? options.config.stream.throttleMs : 1500
85
85
  },
86
- (emit) => runAiImpl(plan.argv, { onChunk: emit }),
86
+ (emit) => runAiImpl(plan.argv, { onChunk: emit, cwd: options.config.repoRoot }),
87
87
  (outbound) => replyOutbound(message, outbound)
88
88
  );
89
89
  }
@@ -100,15 +100,17 @@ export function createMessageDispatcher(options: MessageDispatcherOptions): (mes
100
100
  // - runDaemon() awaits a shutdown promise that only resolves once a
101
101
  // SIGINT/SIGTERM handler has finished graceful cleanup. We never unref()
102
102
  // the only keep-alive timer (that would let the process exit immediately).
103
- export async function runDaemon(): Promise<void> {
103
+ export async function runDaemon({ rootDir }: { rootDir?: string } = {}): Promise<void> {
104
104
  let config;
105
105
  try {
106
- config = loadServerConfig();
106
+ config = loadServerConfig({ rootDir });
107
107
  } catch (error) {
108
108
  process.stderr.write(`${errorMessage(error)}\n`);
109
109
  process.exit(1);
110
110
  }
111
111
 
112
+ process.chdir(config.repoRoot);
113
+
112
114
  const logger = createLogger(config.log);
113
115
  logger.info(`daemon starting agent-infra ${VERSION} pid=${process.pid}`);
114
116
  const ownStartTime = getProcessStartTime(process.pid);
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
2
3
  import { spawn, execFileSync } from 'node:child_process';
3
4
  import { loadServerConfig } from './config.ts';
4
5
  import type { ServerConfig } from './config.ts';
@@ -91,7 +92,7 @@ export async function start({ foreground = false }: StartOptions = {}): Promise<
91
92
  }
92
93
 
93
94
  if (foreground) {
94
- await runDaemon();
95
+ await runDaemon({ rootDir: config.repoRoot });
95
96
  return;
96
97
  }
97
98
 
@@ -103,8 +104,9 @@ export async function start({ foreground = false }: StartOptions = {}): Promise<
103
104
  // Re-spawn ourselves detached. process.execArgv is forwarded so the dev path
104
105
  // (node --experimental-strip-types ./bin/cli.ts) and the built path
105
106
  // (node dist/bin/cli.js) both work.
106
- const child = spawn(process.execPath, [...process.execArgv, cliEntry, 'server', '__daemon'], {
107
+ const child = spawn(process.execPath, [...process.execArgv, path.resolve(cliEntry), 'server', '__daemon'], {
107
108
  detached: true,
109
+ cwd: config.repoRoot,
108
110
  stdio: 'ignore'
109
111
  });
110
112
  child.unref();
@@ -10,8 +10,9 @@ export type RunnerResult = {
10
10
  };
11
11
 
12
12
  export type RunnerOptions = {
13
- spawn?: (file: string, args: string[], onChunk?: (chunk: string) => void | Promise<void>) => Promise<RunnerResult>;
13
+ spawn?: (file: string, args: string[], onChunk?: (chunk: string) => void | Promise<void>, cwd?: string) => Promise<RunnerResult>;
14
14
  onChunk?: (chunk: string) => void | Promise<void>;
15
+ cwd?: string;
15
16
  };
16
17
 
17
18
  function resolveCommand(file: string): string {
@@ -40,13 +41,15 @@ function needsShell(file: string): boolean {
40
41
  function spawnCapture(
41
42
  file: string,
42
43
  args: string[],
43
- onChunk?: (chunk: string) => void | Promise<void>
44
+ onChunk?: (chunk: string) => void | Promise<void>,
45
+ cwd?: string
44
46
  ): Promise<RunnerResult> {
45
47
  return new Promise((resolve, reject) => {
46
48
  const resolvedFile = resolveCommand(file);
47
49
  const child = spawn(resolvedFile, args, {
48
50
  stdio: ['ignore', 'pipe', 'pipe'],
49
- shell: needsShell(resolvedFile)
51
+ shell: needsShell(resolvedFile),
52
+ ...(cwd === undefined ? {} : { cwd })
50
53
  });
51
54
  let stdout = '';
52
55
  let stderr = '';
@@ -91,5 +94,5 @@ function spawnCapture(
91
94
  }
92
95
 
93
96
  export async function runAi(args: string[], options: RunnerOptions = {}): Promise<RunnerResult> {
94
- return (options.spawn ?? spawnCapture)('ai', args, options.onChunk);
97
+ return (options.spawn ?? spawnCapture)('ai', args, options.onChunk, options.cwd);
95
98
  }
@@ -13,10 +13,6 @@
13
13
  "type": "object",
14
14
  "additionalProperties": false,
15
15
  "properties": {
16
- "path": {
17
- "type": "string",
18
- "description": "Log file path. Defaults to ~/.agent-infra/logs/<project>/<repo-hash>/server.log (outside the repo; the repo-hash isolates checkouts that share a project). Relative paths resolve against the repo root; absolute paths are used as-is."
19
- },
20
16
  "rotateAtBytes": {
21
17
  "type": "integer",
22
18
  "minimum": 0,
@@ -9,6 +9,8 @@ import { parseImplementationInputs, selectPendingImplementationInput } from './i
9
9
  import { parseVerdict } from './review-artifacts.ts';
10
10
  import { extractSection, findSectionHeading } from './sections.ts';
11
11
  import { receiptForOutput, sha256File } from './artifact-receipts.ts';
12
+ import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.ts';
13
+ import { inspectReviewIdentity } from './review-identity.ts';
12
14
  import { isArtifactInvalidated, parseInvalidationDocument } from './invalidation.ts';
13
15
  import type { InvalidationDocument } from './invalidation.ts';
14
16
  import type { ArtifactFamily, ArtifactFamilySpec } from './artifact-name.ts';
@@ -228,7 +230,12 @@ function inspectArtifactDirectory(
228
230
  if (!knownRounds.has(round)) diagnostics.push(diagnostic('ROUND_GAP', family, null, `${artifactName(family, round)} is missing`));
229
231
  }
230
232
  const latest = artifacts.at(-1) ?? null;
231
- const reviewedInput = family.startsWith('review-') && latest
233
+ let hasReviewCodeReceipt = false;
234
+ if (family === 'review-code' && latest) {
235
+ try { hasReviewCodeReceipt = Boolean(receiptForOutput(fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8'), latest.name)); }
236
+ catch { hasReviewCodeReceipt = false; }
237
+ }
238
+ const reviewedInput = family.startsWith('review-') && latest && (family !== 'review-code' || hasReviewCodeReceipt)
232
239
  ? resolveReviewedInput(taskDir, latest, family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code', diagnostics)
233
240
  : null;
234
241
  return {
@@ -316,14 +323,16 @@ function assertWritableInventory(inventory: ArtifactInventoryResult): ArtifactEr
316
323
  }
317
324
 
318
325
  const REQUIRED_INPUT: Partial<Record<ArtifactFamily, ArtifactFamily>> = {
319
- 'review-analysis': 'analysis', plan: 'analysis', 'review-plan': 'plan', 'review-code': 'code'
326
+ 'review-analysis': 'analysis', plan: 'analysis', 'review-plan': 'plan'
320
327
  };
321
- const OPTIONAL_CONTEXT: Partial<Record<ArtifactFamily, { family: ArtifactFamily }>> = {
322
- analysis: { family: 'review-analysis' },
323
- plan: { family: 'review-plan' },
324
- 'review-code': { family: 'review-plan' },
325
- 'manual-validation': { family: 'review-code' },
326
- 'validation-run': { family: 'review-code' }
328
+ const OPTIONAL_CONTEXT: Partial<Record<ArtifactFamily, readonly ArtifactFamily[]>> = {
329
+ analysis: ['review-analysis'],
330
+ plan: ['review-analysis', 'review-plan'],
331
+ 'review-plan': ['review-analysis'],
332
+ code: ['review-plan'],
333
+ 'review-code': ['code', 'plan', 'review-plan'],
334
+ 'manual-validation': ['review-code'],
335
+ 'validation-run': ['review-code']
327
336
  };
328
337
  function hasSelectionEvidence(
329
338
  content: string,
@@ -336,6 +345,18 @@ function hasSelectionEvidence(
336
345
  if (family.startsWith('review-')) {
337
346
  const reviewedFamily = family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
338
347
  const currentInput = context.inputs.find((input) => input.family === reviewedFamily);
348
+ if (family === 'review-code') {
349
+ if (!context.latest) return false;
350
+ try {
351
+ const reportContent = fs.readFileSync(context.latest.path, 'utf8');
352
+ const identity = inspectReviewIdentity(context.taskDir!, reportContent, options.repoRoot);
353
+ return identity.status !== 'matched'
354
+ || !hasArtifactCompletionFact(content, context.latest.path, 'review-code.completed')
355
+ || !hasArtifactCompletionLog(content, context.latest.name, 'review-code.completed');
356
+ } catch {
357
+ return true;
358
+ }
359
+ }
339
360
  if (currentInput && context.reviewedInput?.name !== currentInput.name) return true;
340
361
  }
341
362
  const target = family === 'analysis' ? 'analysis' : family === 'plan' ? 'plan' : family === 'code' ? 'code' : null;
@@ -450,9 +471,9 @@ function resolveArtifactContext(taskRef: string, family: string, options: Inspec
450
471
  }
451
472
  const optional = OPTIONAL_CONTEXT[inventory.family as ArtifactFamily];
452
473
  if (optional) {
453
- const context = inspectTaskArtifacts(taskRef, optional.family, options);
454
- if (context.status === 'ready' && context.latest) {
455
- inputs.push(context.latest);
474
+ for (const family of optional) {
475
+ const context = inspectTaskArtifacts(taskRef, family, options);
476
+ if (context.status === 'ready' && context.latest) inputs.push(context.latest);
456
477
  }
457
478
  }
458
479
  if (inventory.family === 'manual-validation' || inventory.family === 'validation-run') {
@@ -473,6 +494,9 @@ function resolveCodeContext(inventory: ArtifactInventoryResult, options: Inspect
473
494
  const codeMax = latestCode?.round ?? 0;
474
495
  const reviewMax = reviewCode.latest?.round ?? 0;
475
496
  const inputs = [source.latest];
497
+ const sourceReviewFamily = inputFamily === 'analysis' ? 'review-analysis' : 'review-plan';
498
+ const sourceReview = inspectTaskArtifacts(taskRef, sourceReviewFamily, options);
499
+ if (sourceReview.status === 'ready' && sourceReview.latest && sourceReview.reviewedInput?.name === source.latest.name) inputs.push(sourceReview.latest);
476
500
  if (latestCode) {
477
501
  try {
478
502
  const taskContent = fs.readFileSync(path.join(inventory.taskDir!, 'task.md'), 'utf8');