@use-the-fork/spindle 0.0.2 → 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.
- package/build/Assets/Blueprints/Catalog.js +19 -9
- package/build/Assets/Blueprints/agents/sdd-implementer.agent.md +6 -6
- package/build/Assets/Blueprints/agents/sdd-orchestrator.agent.md +4 -7
- package/build/Assets/Blueprints/skills/author-agent/SKILL.md +9 -8
- package/build/Assets/Blueprints/skills/author-instruction/SKILL.md +9 -8
- package/build/Assets/Blueprints/skills/author-skill/SKILL.md +9 -8
- package/build/Assets/Blueprints/skills/grill/SKILL.md +3 -3
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/SKILL.md +16 -12
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/assets/implementer-prompt.md +2 -2
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/corrective-rounds.md +1 -1
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/example-workflow.md +14 -12
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/full-spec.md +43 -23
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/review-inputs.md +7 -7
- package/build/Assets/Blueprints/skills/int-testing-agent-guidance/SKILL.md +2 -2
- package/build/Assets/Blueprints/skills/int-writing-skills/SKILL.md +2 -2
- package/build/Assets/Blueprints/skills/int-writing-skills/references/testing-skills.md +2 -2
- package/build/Assets/Blueprints/skills/{asd-ste100 → ref-asd-ste100}/SKILL.md +1 -1
- package/build/Assets/Blueprints/skills/ref-php-laravel-saloon-api-integrations/SKILL.md +38 -0
- package/build/Assets/Blueprints/skills/ref-php-laravel-saloon-api-integrations/references/concurrency-and-pools.md +73 -0
- package/build/Assets/Blueprints/skills/ref-php-laravel-saloon-api-integrations/references/connectors.md +99 -0
- package/build/Assets/Blueprints/skills/ref-php-laravel-saloon-api-integrations/references/data-transfer-objects.md +104 -0
- package/build/Assets/Blueprints/skills/ref-php-laravel-saloon-api-integrations/references/handling-failures.md +95 -0
- package/build/Assets/Blueprints/skills/ref-php-laravel-saloon-api-integrations/references/requests.md +111 -0
- package/build/Assets/Blueprints/skills/ref-php-laravel-saloon-api-integrations/references/responses.md +56 -0
- package/build/Assets/Blueprints/skills/ref-php-laravel-saloon-api-integrations/references/sending-requests.md +42 -0
- package/build/Assets/Blueprints/skills/ref-php-laravel-saloon-api-integrations/references/testing.md +119 -0
- package/build/Assets/Blueprints/skills/{author-design → sdd-author-design}/SKILL.md +12 -4
- package/build/Assets/Blueprints/skills/{grill-design → sdd-grill-design}/SKILL.md +1 -1
- package/build/Assets/Blueprints/skills/{implement-spec → sdd-implement-spec}/SKILL.md +6 -8
- package/build/Assets/Blueprints/skills/{to-spec → sdd-to-spec}/SKILL.md +3 -3
- package/build/Assets/Blueprints/skills/{to-tickets → sdd-to-tickets}/SKILL.md +3 -3
- package/build/Assets/Blueprints/skills/{setup-project → spindle-project-setup}/SKILL.md +4 -4
- package/build/Assets/Blueprints/skills/{update-tooling → spindle-tooling-update}/SKILL.md +5 -5
- package/build/Assets/Templates/project/copilot-instructions.md +3 -9
- package/build/Assets/Templates/project/ticket-tracker.md +14 -6
- package/build/BlueprintManagement/Commands/Prompts.js +1 -1
- package/build/SubagentDrivenDevelopment/Commands/ChecksRecordCommand.js +27 -5
- package/build/SubagentDrivenDevelopment/Commands/ChecksRequireCommand.js +31 -7
- package/build/SubagentDrivenDevelopment/Commands/CorrectCommand.js +0 -1
- package/build/SubagentDrivenDevelopment/Commands/FinishCommand.js +2 -1
- package/build/SubagentDrivenDevelopment/Commands/ImplementCommand.js +2 -0
- package/build/SubagentDrivenDevelopment/Commands/{ClaimCommand.js → IntegrateCommand.js} +8 -7
- package/build/SubagentDrivenDevelopment/Commands/ReviewCommand.js +9 -9
- package/build/SubagentDrivenDevelopment/Commands/StartCommand.js +23 -0
- package/build/SubagentDrivenDevelopment/Commands/StatusCommand.js +5 -0
- package/build/SubagentDrivenDevelopment/Commands/groups.js +3 -2
- package/build/SubagentDrivenDevelopment/Evidence/GitEvidence.js +188 -37
- package/build/SubagentDrivenDevelopment/Models/Workflow.js +0 -2
- package/build/SubagentDrivenDevelopment/Persistence/AssignmentSchema.js +4 -0
- package/build/SubagentDrivenDevelopment/Persistence/CheckSchema.js +4 -13
- package/build/SubagentDrivenDevelopment/Persistence/LedgerRepository.js +66 -5
- package/build/SubagentDrivenDevelopment/Persistence/ProgressSchema.js +54 -4
- package/build/SubagentDrivenDevelopment/Persistence/ReviewSchema.js +4 -0
- package/build/SubagentDrivenDevelopment/Policies/AssignmentPolicy.js +0 -3
- package/build/SubagentDrivenDevelopment/Policies/CheckPolicy.js +6 -3
- package/build/SubagentDrivenDevelopment/Policies/ReadinessPolicy.js +16 -6
- package/build/SubagentDrivenDevelopment/Policies/ReviewPolicy.js +16 -2
- package/build/SubagentDrivenDevelopment/Policies/WorkflowPolicy.js +8 -8
- package/build/SubagentDrivenDevelopment/Services/AssignmentService.js +73 -51
- package/build/SubagentDrivenDevelopment/Services/ChecksService.js +376 -21
- package/build/SubagentDrivenDevelopment/Services/CorrectionService.js +83 -14
- package/build/SubagentDrivenDevelopment/Services/DeliveryCorrectionService.js +3 -7
- package/build/SubagentDrivenDevelopment/Services/DeliveryService.js +43 -38
- package/build/SubagentDrivenDevelopment/Services/FeatureCreationService.js +1 -1
- package/build/SubagentDrivenDevelopment/Services/FeatureFinalizationService.js +1202 -0
- package/build/SubagentDrivenDevelopment/Services/FeatureInspectionService.js +41 -49
- package/build/SubagentDrivenDevelopment/Services/FeatureWorktreeService.js +544 -0
- package/build/SubagentDrivenDevelopment/Services/ReadinessService.js +2 -2
- package/build/SubagentDrivenDevelopment/Services/ReviewService.js +39 -3
- package/build/SubagentDrivenDevelopment/Services/SubmissionService.js +4 -1
- package/build/SubagentDrivenDevelopment/Services/TicketIntegrationService.js +621 -0
- package/build/SubagentDrivenDevelopment/Services/TicketService.js +3 -0
- package/build/SubagentDrivenDevelopment/Services/TicketWorktreeService.js +317 -0
- package/build/SubagentDrivenDevelopment/Services/WorkflowRecordsCommitter.js +336 -0
- package/build/SubagentDrivenDevelopment/SubagentDrivenDevelopmentServiceProvider.js +18 -8
- package/build/TemplateRendering/Computed/ComputedVariableProviders.js +2 -0
- package/build/TemplateRendering/Computed/Providers/AuthoringFlowchartProvider.js +26 -0
- package/package.json +2 -2
- package/build/SubagentDrivenDevelopment/Services/IntegrationService.js +0 -258
- /package/build/Assets/Blueprints/skills/{asd-ste100 → ref-asd-ste100}/CREDITS.MD +0 -0
- /package/build/Assets/Blueprints/skills/{asd-ste100 → ref-asd-ste100}/examples/before-after.md +0 -0
- /package/build/Assets/Blueprints/skills/{asd-ste100 → ref-asd-ste100}/references/writing-rules.md +0 -0
|
@@ -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,
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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 "
|
|
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 "
|
|
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 === "
|
|
56
|
-
|
|
57
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
63
|
+
...this.gitEvidence.contentIdentity(feature, ticket, progress.baseSha, progress.checks),
|
|
91
64
|
};
|
|
92
65
|
}
|
|
93
|
-
if (progress.execution !== "
|
|
94
|
-
throw new Error(`Ticket ${options.ticket}
|
|
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 (
|
|
97
|
-
throw new Error("
|
|
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
|
-
|
|
118
|
-
|
|
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) {
|