@use-the-fork/spindle 0.1.0 → 0.2.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 (60) hide show
  1. package/build/Assets/Blueprints/Catalog.js +7 -2
  2. package/build/Assets/Blueprints/agents/sdd-implementer.agent.md +6 -6
  3. package/build/Assets/Blueprints/agents/sdd-orchestrator.agent.md +4 -7
  4. package/build/Assets/Blueprints/skills/author-agent/SKILL.md +2 -2
  5. package/build/Assets/Blueprints/skills/author-instruction/SKILL.md +2 -2
  6. package/build/Assets/Blueprints/skills/author-skill/SKILL.md +2 -2
  7. package/build/Assets/Blueprints/skills/int-subagent-driven-dev/SKILL.md +13 -9
  8. package/build/Assets/Blueprints/skills/int-subagent-driven-dev/assets/implementer-prompt.md +2 -2
  9. package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/example-workflow.md +11 -9
  10. package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/full-spec.md +33 -13
  11. package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/review-inputs.md +7 -7
  12. package/build/Assets/Blueprints/skills/{asd-ste100 → ref-asd-ste100}/SKILL.md +1 -1
  13. package/build/Assets/Blueprints/skills/sdd-author-design/SKILL.md +2 -2
  14. package/build/Assets/Blueprints/skills/sdd-implement-spec/SKILL.md +4 -6
  15. package/build/Assets/Templates/project/ticket-tracker.md +10 -2
  16. package/build/SubagentDrivenDevelopment/Commands/ChecksRecordCommand.js +27 -5
  17. package/build/SubagentDrivenDevelopment/Commands/ChecksRequireCommand.js +31 -7
  18. package/build/SubagentDrivenDevelopment/Commands/CorrectCommand.js +0 -1
  19. package/build/SubagentDrivenDevelopment/Commands/FinishCommand.js +2 -1
  20. package/build/SubagentDrivenDevelopment/Commands/ImplementCommand.js +2 -0
  21. package/build/SubagentDrivenDevelopment/Commands/{ClaimCommand.js → IntegrateCommand.js} +8 -7
  22. package/build/SubagentDrivenDevelopment/Commands/ReviewCommand.js +9 -9
  23. package/build/SubagentDrivenDevelopment/Commands/StartCommand.js +23 -0
  24. package/build/SubagentDrivenDevelopment/Commands/StatusCommand.js +5 -0
  25. package/build/SubagentDrivenDevelopment/Commands/groups.js +3 -2
  26. package/build/SubagentDrivenDevelopment/Evidence/GitEvidence.js +188 -37
  27. package/build/SubagentDrivenDevelopment/Models/Workflow.js +0 -2
  28. package/build/SubagentDrivenDevelopment/Persistence/AssignmentSchema.js +4 -0
  29. package/build/SubagentDrivenDevelopment/Persistence/CheckSchema.js +4 -13
  30. package/build/SubagentDrivenDevelopment/Persistence/LedgerRepository.js +66 -5
  31. package/build/SubagentDrivenDevelopment/Persistence/ProgressSchema.js +54 -4
  32. package/build/SubagentDrivenDevelopment/Persistence/ReviewSchema.js +4 -0
  33. package/build/SubagentDrivenDevelopment/Policies/AssignmentPolicy.js +0 -3
  34. package/build/SubagentDrivenDevelopment/Policies/CheckPolicy.js +6 -3
  35. package/build/SubagentDrivenDevelopment/Policies/ReadinessPolicy.js +16 -6
  36. package/build/SubagentDrivenDevelopment/Policies/ReviewPolicy.js +16 -2
  37. package/build/SubagentDrivenDevelopment/Policies/WorkflowPolicy.js +8 -8
  38. package/build/SubagentDrivenDevelopment/Services/AssignmentService.js +73 -51
  39. package/build/SubagentDrivenDevelopment/Services/ChecksService.js +376 -21
  40. package/build/SubagentDrivenDevelopment/Services/CorrectionService.js +83 -14
  41. package/build/SubagentDrivenDevelopment/Services/DeliveryCorrectionService.js +3 -7
  42. package/build/SubagentDrivenDevelopment/Services/DeliveryService.js +43 -38
  43. package/build/SubagentDrivenDevelopment/Services/FeatureCreationService.js +1 -1
  44. package/build/SubagentDrivenDevelopment/Services/FeatureFinalizationService.js +1202 -0
  45. package/build/SubagentDrivenDevelopment/Services/FeatureInspectionService.js +41 -49
  46. package/build/SubagentDrivenDevelopment/Services/FeatureWorktreeService.js +544 -0
  47. package/build/SubagentDrivenDevelopment/Services/ReadinessService.js +2 -2
  48. package/build/SubagentDrivenDevelopment/Services/ReviewService.js +39 -3
  49. package/build/SubagentDrivenDevelopment/Services/SubmissionService.js +4 -1
  50. package/build/SubagentDrivenDevelopment/Services/TicketIntegrationService.js +621 -0
  51. package/build/SubagentDrivenDevelopment/Services/TicketService.js +3 -0
  52. package/build/SubagentDrivenDevelopment/Services/TicketWorktreeService.js +317 -0
  53. package/build/SubagentDrivenDevelopment/Services/WorkflowRecordsCommitter.js +336 -0
  54. package/build/SubagentDrivenDevelopment/SubagentDrivenDevelopmentServiceProvider.js +18 -8
  55. package/build/TemplateRendering/Computed/Providers/AuthoringFlowchartProvider.js +1 -1
  56. package/package.json +2 -2
  57. package/build/SubagentDrivenDevelopment/Services/IntegrationService.js +0 -258
  58. /package/build/Assets/Blueprints/skills/{asd-ste100 → ref-asd-ste100}/CREDITS.MD +0 -0
  59. /package/build/Assets/Blueprints/skills/{asd-ste100 → ref-asd-ste100}/examples/before-after.md +0 -0
  60. /package/build/Assets/Blueprints/skills/{asd-ste100 → ref-asd-ste100}/references/writing-rules.md +0 -0
@@ -1,15 +1,47 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { isAbsolute, join } from "node:path";
3
3
  import { blueprintCatalog } from "../../BlueprintManagement/Catalog/BlueprintCatalog.js";
4
4
  import { Service } from "../../Foundation/Service.js";
5
5
  import { FEATURE_DIRECTORY, EXECUTION_STATES, TICKET_CATEGORIES, TRACKER_STATUSES, } from "../Models/Workflow.js";
6
6
  import { isImplementationAssignment, isCorrectionAttempt, isCorrectionAdjudication, hasMatchingCorrectionAttempt, } from "./AssignmentSchema.js";
7
- import { isCheckRequirement, isCheckRecord, isCheckGateInvalidation, } from "./CheckSchema.js";
7
+ import { isCheckRequirement, isCheckRecord, } from "./CheckSchema.js";
8
8
  import { isReviewFinding, isRequirementsDecision, } from "./FindingSchema.js";
9
9
  import { isDeliveryProgress, isAcceptedEvidence, isIntegrationRecord, } from "./ProgressSchema.js";
10
10
  import { isReviewAssignment, isSpecialistReviewRequirement, } from "./ReviewSchema.js";
11
11
  import { Records } from "../../Support/Records.js";
12
12
  const isRecord = Records.isRecord;
13
+ function matchingCorrectionChecks(current, previous) {
14
+ const expected = previous.postMerge?.checks;
15
+ const checks = current.postMerge?.checks;
16
+ return (!!expected &&
17
+ !!checks &&
18
+ JSON.stringify(expected.required.map((item) => item.command)) ===
19
+ JSON.stringify(checks.required.map((item) => item.command)));
20
+ }
21
+ function matchingCorrectionHistory(history) {
22
+ const first = history[0];
23
+ return !!first && history.every((integration) => matchingCorrectionChecks(integration, first));
24
+ }
25
+ function validBaselineHistory(value, baseSha) {
26
+ if (!Array.isArray(value) ||
27
+ value.length === 0 ||
28
+ !value.every((change) => isRecord(change) &&
29
+ typeof change.from === "string" &&
30
+ /^[0-9a-f]{40,64}$/i.test(change.from) &&
31
+ typeof change.to === "string" &&
32
+ /^[0-9a-f]{40,64}$/i.test(change.to) &&
33
+ change.from !== change.to &&
34
+ typeof change.ticketHead === "string" &&
35
+ /^[0-9a-f]{40,64}$/i.test(change.ticketHead) &&
36
+ typeof change.at === "string" &&
37
+ Number.isFinite(Date.parse(change.at)) &&
38
+ typeof change.author === "string" &&
39
+ !!change.author.trim() &&
40
+ Object.keys(change).every((key) => ["from", "to", "ticketHead", "at", "author"].includes(key))))
41
+ return false;
42
+ return (value.every((change, index) => index === 0 || value[index - 1].to === change.from) &&
43
+ value.at(-1).to === baseSha);
44
+ }
13
45
  export class LedgerRepository extends Service {
14
46
  constructor(app) {
15
47
  super(app);
@@ -81,6 +113,16 @@ export class LedgerRepository extends Service {
81
113
  !!ticket.owner.trim() &&
82
114
  typeof ticket.baseSha === "string" &&
83
115
  /^[0-9a-f]{40,64}$/i.test(ticket.baseSha))) &&
116
+ (!Object.hasOwn(ticket, "baselineHistory") ||
117
+ validBaselineHistory(ticket.baselineHistory, ticket.baseSha)) &&
118
+ (!Object.hasOwn(ticket, "worktree") ||
119
+ (isRecord(ticket.worktree) &&
120
+ typeof ticket.worktree.workspace === "string" &&
121
+ isAbsolute(ticket.worktree.workspace) &&
122
+ ticket.worktree.branch === `tickets/${feature}/${ticket.id}` &&
123
+ typeof ticket.worktree.baseSha === "string" &&
124
+ ticket.worktree.baseSha === ticket.baseSha &&
125
+ /^[0-9a-f]{40,64}$/i.test(ticket.worktree.baseSha))) &&
84
126
  (!Object.hasOwn(ticket, "acceptedEvidence") ||
85
127
  isAcceptedEvidence(ticket.acceptedEvidence)) &&
86
128
  (!Object.hasOwn(ticket, "integration") ||
@@ -90,6 +132,23 @@ export class LedgerRepository extends Service {
90
132
  JSON.stringify(ticket.integration.files) ===
91
133
  JSON.stringify(ticket.acceptedEvidence.files) &&
92
134
  ticket.integration.contentDigest === ticket.acceptedEvidence.contentDigest)) &&
135
+ (!Object.hasOwn(ticket, "integrationHistory") ||
136
+ (Array.isArray(ticket.integrationHistory) &&
137
+ ticket.integrationHistory.length > 0 &&
138
+ ticket.integrationHistory.every(isIntegrationRecord) &&
139
+ ticket.integrationHistory.every((integration) => {
140
+ const postMerge = integration.postMerge;
141
+ const records = postMerge?.checks?.records ?? [];
142
+ return (postMerge?.status === "pending" &&
143
+ records.some((record, index) => record.result === "failed" &&
144
+ !records.slice(index + 1).some((later) => later.command === record.command)));
145
+ }) &&
146
+ new Set(ticket.integrationHistory.map((integration) => integration.mergeSha)).size ===
147
+ ticket.integrationHistory.length &&
148
+ (!isIntegrationRecord(ticket.integration) ||
149
+ (!new Set(ticket.integrationHistory.map((integration) => integration.mergeSha)).has(ticket.integration.mergeSha) &&
150
+ matchingCorrectionChecks(ticket.integration, ticket.integrationHistory[ticket.integrationHistory.length - 1]))) &&
151
+ matchingCorrectionHistory(ticket.integrationHistory))) &&
93
152
  (!Object.hasOwn(ticket, "reviewRequirements") ||
94
153
  (Array.isArray(ticket.reviewRequirements) &&
95
154
  ticket.reviewRequirements.every(isSpecialistReviewRequirement) &&
@@ -101,11 +160,13 @@ export class LedgerRepository extends Service {
101
160
  ticket.checks.required.every(isCheckRequirement) &&
102
161
  Array.isArray(ticket.checks.records) &&
103
162
  ticket.checks.records.every(isCheckRecord) &&
104
- (!Object.hasOwn(ticket.checks, "invalidations") ||
105
- (Array.isArray(ticket.checks.invalidations) &&
106
- ticket.checks.invalidations.every(isCheckGateInvalidation))))) &&
163
+ !Object.hasOwn(ticket.checks, "invalidations"))) &&
107
164
  (!Object.hasOwn(ticket, "assignment") ||
108
165
  (isImplementationAssignment(ticket.assignment) &&
166
+ (!ticket.worktree ||
167
+ (isRecord(ticket.worktree) &&
168
+ ticket.assignment.workspace === ticket.worktree.workspace &&
169
+ ticket.assignment.branch === ticket.worktree.branch)) &&
109
170
  ticket.assignment.report ===
110
171
  join(this.app.workspaceRoot, ".scratch", feature, "reports", ticket.assignment.action === "implement"
111
172
  ? `${ticket.id}-implementation.md`
@@ -1,7 +1,7 @@
1
1
  import { isAbsolute } from "node:path";
2
2
  import { EXECUTION_STATES } from "../Models/Workflow.js";
3
3
  import { isImplementationAssignment, isCorrectionAttempt, isCorrectionAdjudication, } from "./AssignmentSchema.js";
4
- import { isCheckRequirement, isCheckRecord, isCheckGateInvalidation, isSafeCoveragePath, } from "./CheckSchema.js";
4
+ import { isCheckRequirement, isCheckRecord, isSafeCoveragePath, } from "./CheckSchema.js";
5
5
  import { isReviewFinding, isRequirementsDecision, } from "./FindingSchema.js";
6
6
  import { isReviewAssignment, isSpecialistReviewRequirement, } from "./ReviewSchema.js";
7
7
  import { Records } from "../../Support/Records.js";
@@ -41,9 +41,7 @@ export function isDeliveryProgress(value) {
41
41
  value.checks.required.every(isCheckRequirement) &&
42
42
  Array.isArray(value.checks.records) &&
43
43
  value.checks.records.every(isCheckRecord) &&
44
- (!Object.hasOwn(value.checks, "invalidations") ||
45
- (Array.isArray(value.checks.invalidations) &&
46
- value.checks.invalidations.every(isCheckGateInvalidation))))) &&
44
+ !Object.hasOwn(value.checks, "invalidations"))) &&
47
45
  (!Object.hasOwn(value, "findings") ||
48
46
  (Array.isArray(value.findings) && value.findings.every(isReviewFinding))) &&
49
47
  (!Object.hasOwn(value, "correctionAttempts") ||
@@ -82,6 +80,17 @@ export function isIntegrationRecord(value) {
82
80
  return (isRecord(value) &&
83
81
  typeof value.commitSha === "string" &&
84
82
  /^[0-9a-f]{40,64}$/i.test(value.commitSha) &&
83
+ (!Object.hasOwn(value, "mergeSha") ||
84
+ (typeof value.mergeSha === "string" && /^[0-9a-f]{40,64}$/i.test(value.mergeSha))) &&
85
+ (!Object.hasOwn(value, "postMerge") ||
86
+ (isRecord(value.postMerge) &&
87
+ Object.keys(value.postMerge).every((key) => ["status", "mergeSha", "checks"].includes(key)) &&
88
+ (value.postMerge.status === "pending" || value.postMerge.status === "passed") &&
89
+ typeof value.mergeSha === "string" &&
90
+ value.postMerge.mergeSha === value.mergeSha &&
91
+ (!Object.hasOwn(value.postMerge, "checks") ||
92
+ isPostMergeChecks(value.postMerge.checks, value.mergeSha, value.postMerge.status)) &&
93
+ (value.postMerge.status !== "passed" || Object.hasOwn(value.postMerge, "checks")))) &&
85
94
  typeof value.integratedAt === "string" &&
86
95
  Number.isFinite(Date.parse(value.integratedAt)) &&
87
96
  typeof value.integratedBy === "string" &&
@@ -93,3 +102,44 @@ export function isIntegrationRecord(value) {
93
102
  typeof value.contentDigest === "string" &&
94
103
  /^[0-9a-f]{64}$/i.test(value.contentDigest));
95
104
  }
105
+ function isPostMergeChecks(value, mergeSha, status) {
106
+ if (!isRecord(value) ||
107
+ !Object.keys(value).every((key) => ["required", "records"].includes(key)) ||
108
+ !Array.isArray(value.required) ||
109
+ value.required.length === 0 ||
110
+ !value.required.every(isCheckRequirement) ||
111
+ value.required.some((item) => item.command !== item.command.trim() ||
112
+ !Object.keys(item).every((key) => ["command", "addedAt", "addedBy"].includes(key))) ||
113
+ value.required.some((item) => item.removedAt !== undefined ||
114
+ item.removedBy !== undefined ||
115
+ item.removalDetails !== undefined) ||
116
+ new Set(value.required.map((item) => item.command)).size !== value.required.length ||
117
+ !Array.isArray(value.records) ||
118
+ !value.records.every(isCheckRecord) ||
119
+ value.records.some((record) => record.headSha !== mergeSha ||
120
+ !Object.keys(record).every((key) => [
121
+ "command",
122
+ "result",
123
+ "author",
124
+ "snapshot",
125
+ "codeSnapshot",
126
+ "requirementsSnapshot",
127
+ "headSha",
128
+ "recordedAt",
129
+ "details",
130
+ ].includes(key)) ||
131
+ record.coveredPaths !== undefined ||
132
+ record.coverageSnapshot !== undefined))
133
+ return false;
134
+ const required = value.required;
135
+ if (value.records.some((record) => !required.some((item) => item.command === record.command)))
136
+ return false;
137
+ if (status === "pending")
138
+ return true;
139
+ const records = value.records;
140
+ const latest = value.required.map((item) => [...records].reverse().find((record) => record.command === item.command));
141
+ return latest.every((record) => record?.result === "passed" &&
142
+ record.snapshot === latest[0]?.snapshot &&
143
+ record.codeSnapshot === latest[0]?.codeSnapshot &&
144
+ record.requirementsSnapshot === latest[0]?.requirementsSnapshot);
145
+ }
@@ -23,6 +23,10 @@ export function isReviewAssignment(value) {
23
23
  !/^[0-9a-f]{64}$/i.test(value.requirementsSnapshot) ||
24
24
  typeof value.headSha !== "string" ||
25
25
  !/^[0-9a-f]{40,64}$/i.test(value.headSha) ||
26
+ (Object.hasOwn(value, "workspace") &&
27
+ (typeof value.workspace !== "string" || !isAbsolute(value.workspace))) ||
28
+ (Object.hasOwn(value, "branch") &&
29
+ (typeof value.branch !== "string" || !value.branch.trim())) ||
26
30
  !Array.isArray(value.inventory) ||
27
31
  !value.inventory.every((path) => typeof path === "string" && path.length > 0) ||
28
32
  new Set(value.inventory).size !== value.inventory.length ||
@@ -21,9 +21,6 @@ export class AssignmentPolicy extends Service {
21
21
  return undefined;
22
22
  }
23
23
  resumeBlocker(feature, progress) {
24
- if (progress.execution === "claimed" && (!progress.owner || !progress.baseSha)) {
25
- return "Claim has no recorded owner or baseline; implementation is unavailable.";
26
- }
27
24
  if (progress.execution === "implementing" && !progress.assignment) {
28
25
  return "Implementation is marked in progress without a recorded assignment.";
29
26
  }
@@ -19,18 +19,21 @@ export class CheckPolicy extends Service {
19
19
  const records = checks.records.filter((record) => record.command === requirement.command);
20
20
  const preservedIntegrationEvidence = this.gitEvidence.integrationPreservesEvidence(progress, identity);
21
21
  const current = records.filter((record) => {
22
- if (record.snapshot === identity.snapshot &&
22
+ if ((!progress.worktree || record.headSha === identity.headSha) &&
23
+ record.snapshot === identity.snapshot &&
23
24
  record.codeSnapshot === identity.codeSnapshot &&
24
25
  record.requirementsSnapshot === identity.requirementsSnapshot) {
25
26
  return true;
26
27
  }
27
- if (preservedIntegrationEvidence &&
28
+ if (!progress.worktree &&
29
+ preservedIntegrationEvidence &&
28
30
  record.snapshot === progress.acceptedEvidence?.snapshot &&
29
31
  record.codeSnapshot === progress.acceptedEvidence?.codeSnapshot &&
30
32
  record.requirementsSnapshot === progress.acceptedEvidence?.requirementsSnapshot) {
31
33
  return true;
32
34
  }
33
- if (!record.coveredPaths ||
35
+ if ((progress.worktree && record.headSha !== identity.headSha) ||
36
+ !record.coveredPaths ||
34
37
  !record.coverageSnapshot ||
35
38
  record.requirementsSnapshot !== identity.requirementsSnapshot) {
36
39
  return false;
@@ -5,17 +5,27 @@ export class ReadinessPolicy extends Service {
5
5
  super(app);
6
6
  this.ticketRepository = ticketRepository;
7
7
  }
8
- evaluate(tickets) {
9
- this.ticketRepository.validateDependencyGraph(tickets);
8
+ static unmetDependencies(ticket, tickets, ledger) {
10
9
  const byId = new Map(tickets.map((item) => [item.id, item]));
10
+ const progressById = new Map(ledger.tickets.map((item) => [item.id, item]));
11
+ return ticket.blocked_by.filter((id) => {
12
+ const dependencyTicket = byId.get(id);
13
+ const progress = progressById.get(id);
14
+ return (!dependencyTicket ||
15
+ dependencyTicket.status !== "closed" ||
16
+ !!dependencyTicket.blocker ||
17
+ progress?.status !== "closed" ||
18
+ progress.execution !== "integrated" ||
19
+ progress.integration?.postMerge?.status !== "passed");
20
+ });
21
+ }
22
+ evaluate(tickets, ledger) {
23
+ this.ticketRepository.validateDependencyGraph(tickets);
11
24
  const eligible = tickets.filter((item) => item.status === "open" && !item.blocker);
12
25
  const ready = [];
13
26
  const unready = [];
14
27
  for (const item of eligible) {
15
- const pending = item.blocked_by.filter((id) => {
16
- const dependencyTicket = byId.get(id);
17
- return (!dependencyTicket || dependencyTicket.status !== "closed" || !!dependencyTicket.blocker);
18
- });
28
+ const pending = ReadinessPolicy.unmetDependencies(item, tickets, ledger);
19
29
  if (pending.length === 0)
20
30
  ready.push(item.id);
21
31
  else
@@ -33,7 +33,16 @@ export class ReviewPolicy extends Service {
33
33
  return undefined;
34
34
  }
35
35
  reviewSnapshotBlocker(feature, progress, assignment, identity) {
36
+ if ("id" in progress && progress.worktree) {
37
+ try {
38
+ this.gitEvidence.assertTicketCodeCommitted(progress);
39
+ }
40
+ catch (error) {
41
+ return `Ticket review workspace is invalid: ${error instanceof Error ? error.message : String(error)}`;
42
+ }
43
+ }
36
44
  const integratedEvidence = assignment.scope !== "delivery" &&
45
+ !("worktree" in progress && progress.worktree) &&
37
46
  this.gitEvidence.integrationPreservesEvidence(progress, identity) &&
38
47
  assignment.snapshot === progress.acceptedEvidence?.snapshot &&
39
48
  assignment.codeSnapshot === progress.acceptedEvidence?.codeSnapshot &&
@@ -42,14 +51,19 @@ export class ReviewPolicy extends Service {
42
51
  if (!integratedEvidence &&
43
52
  (assignment.snapshot !== identity.snapshot ||
44
53
  assignment.codeSnapshot !== identity.codeSnapshot ||
45
- assignment.requirementsSnapshot !== identity.requirementsSnapshot)) {
54
+ assignment.requirementsSnapshot !== identity.requirementsSnapshot ||
55
+ ("worktree" in progress &&
56
+ !!progress.worktree &&
57
+ (assignment.headSha !== identity.headSha ||
58
+ assignment.workspace !== progress.worktree.workspace ||
59
+ assignment.branch !== progress.worktree.branch)))) {
46
60
  return `Review assignment ${assignment.id} is stale for the current ticket snapshot.`;
47
61
  }
48
62
  if (!progress.baseSha)
49
63
  return "Review assignment has no recorded Git baseline.";
50
64
  let inventory;
51
65
  try {
52
- inventory = this.gitEvidence.changedPaths(progress.baseSha);
66
+ inventory = this.gitEvidence.changedPaths(progress.baseSha, "id" in progress ? this.gitEvidence.ticketWorkspace(progress) : undefined);
53
67
  }
54
68
  catch (error) {
55
69
  return `Review inventory cannot be revalidated: ${error instanceof Error ? error.message : String(error)}`;
@@ -7,10 +7,7 @@ export class WorkflowPolicy extends Service {
7
7
  if (progress.adjudication?.status === "pending")
8
8
  return "adjudicate";
9
9
  if (progress.execution === "unstarted")
10
- return "claim";
11
- if (progress.execution === "claimed") {
12
- return progress.owner && progress.baseSha ? "implement" : "resolve-claim-blocker";
13
- }
10
+ return "implement";
14
11
  if (progress.execution === "implementing") {
15
12
  if (!progress.assignment)
16
13
  return "resolve-assignment-blocker";
@@ -38,7 +35,7 @@ export class WorkflowPolicy extends Service {
38
35
  if (reviews.some((assignment) => assignment.result !== "passed")) {
39
36
  return "resolve-review-result";
40
37
  }
41
- return "accept";
38
+ return "integrate";
42
39
  }
43
40
  return blocker ? "restore-implementation-report" : "review";
44
41
  }
@@ -52,10 +49,13 @@ export class WorkflowPolicy extends Service {
52
49
  ? "correct"
53
50
  : "controller-correction-decision";
54
51
  }
55
- if (progress.execution === "accepted")
56
- return blocker ? "resolve-check-blocker" : "integrate";
57
- if (progress.execution === "integrated")
52
+ if (progress.execution === "integrated") {
53
+ if (progress.integration?.postMerge?.status !== "passed")
54
+ return "post-merge-checks";
55
+ if (progress.status !== "closed")
56
+ return "verify-ticket-closure";
58
57
  return blocker ? "resolve-check-blocker" : "complete";
58
+ }
59
59
  return "resolve-blocker";
60
60
  }
61
61
  requireProgress(ledger, ticket) {
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { join } from "node:path";
2
+ import { realpathSync } from "node:fs";
3
+ import { isAbsolute, join } from "node:path";
3
4
  import matter from "gray-matter";
4
5
  import { Service } from "../../Foundation/Service.js";
5
6
  import { lstatIfPresent } from "../Persistence/FileSystem.js";
@@ -13,9 +14,12 @@ export class AssignmentService extends Service {
13
14
  assignmentPolicy;
14
15
  checkPolicy;
15
16
  reviewPolicy;
17
+ readinessPolicy;
16
18
  workflowPolicy;
17
19
  featureInspectionService;
18
- constructor(app, gitEvidence, reportEvidence, ticketRepository, ledgerRepository, workspaceTransaction, assignmentPolicy, checkPolicy, reviewPolicy, workflowPolicy, featureInspectionService) {
20
+ ticketWorktreeService;
21
+ workflowRecordsCommitter;
22
+ constructor(app, gitEvidence, reportEvidence, ticketRepository, ledgerRepository, workspaceTransaction, assignmentPolicy, checkPolicy, reviewPolicy, readinessPolicy, workflowPolicy, featureInspectionService, ticketWorktreeService, workflowRecordsCommitter) {
19
23
  super(app);
20
24
  this.gitEvidence = gitEvidence;
21
25
  this.reportEvidence = reportEvidence;
@@ -25,10 +29,13 @@ export class AssignmentService extends Service {
25
29
  this.assignmentPolicy = assignmentPolicy;
26
30
  this.checkPolicy = checkPolicy;
27
31
  this.reviewPolicy = reviewPolicy;
32
+ this.readinessPolicy = readinessPolicy;
28
33
  this.workflowPolicy = workflowPolicy;
29
34
  this.featureInspectionService = featureInspectionService;
35
+ this.ticketWorktreeService = ticketWorktreeService;
36
+ this.workflowRecordsCommitter = workflowRecordsCommitter;
30
37
  }
31
- claim(options) {
38
+ implement(options) {
32
39
  validateTicketId(options.ticket);
33
40
  validateAuthor(options.author);
34
41
  const feature = this.featureInspectionService.resolveFeature(options.spec);
@@ -38,84 +45,98 @@ export class AssignmentService extends Service {
38
45
  this.ticketRepository.validateDependencyGraph(tickets);
39
46
  const ticket = this.ticketRepository.requireTicket(tickets, options.ticket);
40
47
  const progress = this.workflowPolicy.requireProgress(ledger, options.ticket);
41
- if (progress.execution !== "unstarted") {
42
- throw new Error(`Ticket ${options.ticket} is already owned or has progressed to ${progress.execution}.`);
43
- }
44
- if (ticket.status !== "open" || ticket.blocker) {
45
- throw new Error(`Ticket ${options.ticket} is not open and unblocked.`);
46
- }
47
- const ticketsById = new Map(tickets.map((item) => [item.id, item]));
48
- const unmet = ticket.blocked_by.filter((dependency) => {
49
- const dependencyTicket = ticketsById.get(dependency);
50
- return (!dependencyTicket || dependencyTicket.status !== "closed" || !!dependencyTicket.blocker);
51
- });
52
- if (unmet.length > 0) {
53
- throw new Error(`Ticket ${options.ticket} has open dependencies: ${unmet.join(", ")}.`);
54
- }
55
- const baseSha = this.gitEvidence.cleanGitBaseline(feature);
56
- progress.execution = "claimed";
57
- progress.status = "in-progress";
58
- progress.owner = options.author.trim();
59
- progress.baseSha = baseSha;
60
- ledger.delivery.baseSha ??= baseSha;
61
- const identity = this.gitEvidence.contentIdentity(feature, ticket, baseSha, progress.checks);
62
- const afterTicket = matter.stringify(this.ticketRepository.appendAudit(ticket.body, options.author, `Claimed from clean Git baseline ${baseSha}.`), this.ticketRepository.metadata(ticket));
63
- this.workspaceTransaction.writeWorkflowState(feature, ticket, afterTicket, ledger);
64
- return {
65
- ticket: options.ticket,
66
- execution: "claimed",
67
- owner: options.author.trim(),
68
- baseSha,
69
- ...identity,
70
- };
71
- });
72
- }
73
- implement(options) {
74
- validateTicketId(options.ticket);
75
- validateAuthor(options.author);
76
- const feature = this.featureInspectionService.resolveFeature(options.spec);
77
- return this.workspaceTransaction.run(feature.workspace, () => {
78
- const ledger = this.ledgerRepository.readLedger(feature);
79
- const ticket = this.ticketRepository.requireTicket(this.ticketRepository.readTickets(feature.workspace), options.ticket);
80
- const progress = this.workflowPolicy.requireProgress(ledger, options.ticket);
81
- const identity = this.gitEvidence.contentIdentity(feature, ticket, progress.baseSha, progress.checks);
82
48
  if (progress.execution === "implementing" && progress.assignment) {
83
49
  if (progress.owner !== options.author.trim()) {
84
50
  throw new Error("Controller ownership changed; resume and confirm the handoff first.");
85
51
  }
52
+ if (!progress.worktree ||
53
+ !options.workspace ||
54
+ !isAbsolute(options.workspace) ||
55
+ join(realpathSync(options.workspace), `${feature.feature}-${options.ticket}`) !==
56
+ progress.worktree.workspace) {
57
+ throw new Error("Resume implementation using the originally assigned --workspace directory.");
58
+ }
86
59
  return {
87
60
  ticket: options.ticket,
88
61
  execution: "implementing",
89
62
  assignment: progress.assignment,
90
- ...identity,
63
+ ...this.gitEvidence.contentIdentity(feature, ticket, progress.baseSha, progress.checks),
91
64
  };
92
65
  }
93
- if (progress.execution !== "claimed" || !progress.baseSha || !progress.owner) {
94
- throw new Error(`Ticket ${options.ticket} must be claimed before implementation dispatch.`);
66
+ if (progress.execution !== "unstarted") {
67
+ throw new Error(`Ticket ${options.ticket} is already owned or has progressed to ${progress.execution}.`);
68
+ }
69
+ if (ticket.status !== "open" || ticket.blocker) {
70
+ throw new Error(`Ticket ${options.ticket} is not open and unblocked.`);
71
+ }
72
+ const readiness = this.readinessPolicy.evaluate(tickets, ledger);
73
+ if (!readiness.ready.includes(options.ticket)) {
74
+ const unready = readiness.unready.find((item) => item.ticket === options.ticket);
75
+ const dependencies = unready?.blockedBy ?? ticket.blocked_by;
76
+ throw new Error(dependencies.length > 0
77
+ ? `Ticket ${options.ticket} has open dependencies: ${dependencies.join(", ")}.`
78
+ : `Ticket ${options.ticket} is not ready for implementation.`);
95
79
  }
96
- if (progress.owner !== options.author.trim()) {
97
- throw new Error("Only the recorded controller may dispatch implementation.");
80
+ if (!options.workspace || !isAbsolute(options.workspace)) {
81
+ throw new Error("--workspace must be an absolute existing external directory.");
98
82
  }
99
83
  const report = join(feature.workspace, "reports", `${options.ticket}-implementation.md`);
100
84
  this.reportEvidence.assertReportPath(feature, report, false);
101
85
  if (lstatIfPresent(report)) {
102
86
  throw new Error(`Assigned report already exists and was preserved: ${report}.`);
103
87
  }
88
+ const pendingPostMerge = ledger.tickets.some((candidate) => (candidate.integration ?? candidate.integrationHistory?.at(-1))?.postMerge?.status ===
89
+ "pending");
90
+ if (!pendingPostMerge) {
91
+ this.workflowRecordsCommitter.commit(feature, options.confirmRecords === true);
92
+ }
93
+ const baseSha = this.gitEvidence.cleanDispatchBaseline(feature, pendingPostMerge);
94
+ progress.status = "in-progress";
95
+ progress.owner = options.author.trim();
96
+ progress.baseSha = baseSha;
97
+ ledger.delivery.baseSha ??= baseSha;
98
+ const requiredAncestors = ticket.blocked_by.map((id) => {
99
+ const dependency = this.workflowPolicy.requireProgress(ledger, id);
100
+ const mergeSha = dependency.integration?.mergeSha;
101
+ if (!mergeSha || dependency.integration?.postMerge?.status !== "passed") {
102
+ throw new Error(`Ticket ${id} is not integrated and post-merge validated.`);
103
+ }
104
+ return mergeSha;
105
+ });
106
+ const worktree = this.ticketWorktreeService.create(feature, progress, options.workspace, requiredAncestors, pendingPostMerge);
104
107
  const assignment = {
105
108
  id: randomUUID(),
106
109
  role: "implementation",
107
110
  action: "implement",
108
111
  controller: options.author.trim(),
109
112
  report,
113
+ workspace: worktree.workspace,
114
+ branch: worktree.branch,
110
115
  requestedAt: new Date().toISOString(),
111
116
  status: "pending",
112
117
  handoffs: [],
113
118
  };
114
119
  progress.execution = "implementing";
115
120
  progress.assignment = assignment;
121
+ progress.baseSha = worktree.baseSha;
122
+ progress.worktree = {
123
+ workspace: worktree.workspace,
124
+ branch: worktree.branch,
125
+ baseSha: worktree.baseSha,
126
+ };
116
127
  const afterTicket = matter.stringify(this.ticketRepository.appendAudit(ticket.body, options.author, `Implementation assignment ${assignment.id} recorded; role=implementation; report=${report}. No agent was launched.`), this.ticketRepository.metadata(ticket));
117
- this.workspaceTransaction.writeWorkflowState(feature, ticket, afterTicket, ledger);
118
- return { ticket: options.ticket, execution: "implementing", assignment, ...identity };
128
+ try {
129
+ this.workspaceTransaction.writeWorkflowState(feature, ticket, afterTicket, ledger);
130
+ }
131
+ catch (error) {
132
+ throw new Error(`Assignment recording failed; preserve and inspect ${worktree.journal}. ${error instanceof Error ? error.message : String(error)}`);
133
+ }
134
+ return {
135
+ ticket: options.ticket,
136
+ execution: "implementing",
137
+ assignment,
138
+ ...this.gitEvidence.contentIdentity(feature, ticket, progress.baseSha, progress.checks),
139
+ };
119
140
  });
120
141
  }
121
142
  resume(options) {
@@ -135,6 +156,7 @@ export class AssignmentService extends Service {
135
156
  const ledger = this.ledgerRepository.readLedger(feature);
136
157
  const ticket = this.ticketRepository.requireTicket(this.ticketRepository.readTickets(feature.workspace), options.ticket);
137
158
  const progress = this.workflowPolicy.requireProgress(ledger, options.ticket);
159
+ this.gitEvidence.ticketWorkspace(progress);
138
160
  let handoffRecorded = false;
139
161
  if (progress.owner && progress.owner !== options.author.trim()) {
140
162
  if (!options.confirmHandoff) {