@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
|
@@ -5,16 +5,19 @@ import { ReviewService } from "../Services/ReviewService.js";
|
|
|
5
5
|
export class ReviewCommand extends Command {
|
|
6
6
|
static register(root, createApp) {
|
|
7
7
|
reviewSchema(root).action(async (action, options) => {
|
|
8
|
+
if (options.scope !== undefined && options.scope !== "delivery") {
|
|
9
|
+
throw new Error("--scope must be 'delivery'.");
|
|
10
|
+
}
|
|
11
|
+
if (Boolean(options.ticket) === (options.scope === "delivery")) {
|
|
12
|
+
throw new Error("review needs exactly one of --ticket or --scope delivery.");
|
|
13
|
+
}
|
|
8
14
|
if (action === "require") {
|
|
9
15
|
if (options.type && options.agent && options.type !== options.agent) {
|
|
10
16
|
throw new Error("--type and --agent must identify the same review agent.");
|
|
11
17
|
}
|
|
12
18
|
const reviewType = options.type ?? options.agent;
|
|
13
|
-
if (options.scope !== "delivery") {
|
|
14
|
-
throw new Error("Feature review requirements need --scope delivery.");
|
|
15
|
-
}
|
|
16
19
|
if (!options.spec || !reviewType || !options.details || !options.author) {
|
|
17
|
-
throw new Error("review require needs --spec, --
|
|
20
|
+
throw new Error("review require needs --spec, --type, --details, and --author.");
|
|
18
21
|
}
|
|
19
22
|
if (options.inventory)
|
|
20
23
|
throw new Error("--inventory is not valid with 'review require'.");
|
|
@@ -33,11 +36,8 @@ export class ReviewCommand extends Command {
|
|
|
33
36
|
if (options.type || options.agent || options.details) {
|
|
34
37
|
throw new Error("--type, --agent, and --details are only valid with 'review require'.");
|
|
35
38
|
}
|
|
36
|
-
if (!options.spec ||
|
|
37
|
-
|
|
38
|
-
!options.inventory ||
|
|
39
|
-
!options.author) {
|
|
40
|
-
throw new Error("Feature review requires --spec, --scope delivery, --inventory, and --author.");
|
|
39
|
+
if (!options.spec || !options.inventory || !options.author) {
|
|
40
|
+
throw new Error("review requires --spec, --inventory, and --author.");
|
|
41
41
|
}
|
|
42
42
|
const command = await (await createApp()).make(ReviewCommand);
|
|
43
43
|
console.log(JSON.stringify(await command.run({
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Command } from "../../Commands/Command.js";
|
|
2
|
+
import { CommandOptions } from "../../Commands/CommandOptions.js";
|
|
3
|
+
import { group } from "./groups.js";
|
|
4
|
+
import { FeatureWorktreeService } from "../Services/FeatureWorktreeService.js";
|
|
5
|
+
export class StartCommand extends Command {
|
|
6
|
+
static register(root, createApp) {
|
|
7
|
+
group(root)
|
|
8
|
+
.command("start")
|
|
9
|
+
.description("Commit feature records and create a feature branch and worktree.")
|
|
10
|
+
.requiredOption("--spec <path>", "Feature spec path", CommandOptions.once("--spec"))
|
|
11
|
+
.requiredOption("--workspace <directory>", "Existing absolute external directory for the new worktree", CommandOptions.once("--workspace"))
|
|
12
|
+
.requiredOption("--confirm-records", "Confirm feature records were inspected for sensitive content")
|
|
13
|
+
.allowExcessArguments(false)
|
|
14
|
+
.action(async (options) => {
|
|
15
|
+
const command = await (await createApp()).make(StartCommand);
|
|
16
|
+
console.log(JSON.stringify(await command.run(options), null, 2));
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
async run(options) {
|
|
20
|
+
this.assertApplicationBooted();
|
|
21
|
+
return (await this.app.make(FeatureWorktreeService)).start(options);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "../../Commands/Command.js";
|
|
2
2
|
import { CommandOptions } from "../../Commands/CommandOptions.js";
|
|
3
3
|
import { group } from "./groups.js";
|
|
4
|
+
import { FeatureFinalizationService } from "../Services/FeatureFinalizationService.js";
|
|
4
5
|
import { FeatureInspectionService } from "../Services/FeatureInspectionService.js";
|
|
5
6
|
export class StatusCommand extends Command {
|
|
6
7
|
static register(root, createApp) {
|
|
@@ -16,6 +17,10 @@ export class StatusCommand extends Command {
|
|
|
16
17
|
}
|
|
17
18
|
async run(options) {
|
|
18
19
|
this.assertApplicationBooted();
|
|
20
|
+
const finalization = await this.app.make(FeatureFinalizationService);
|
|
21
|
+
const recorded = finalization.recordedFeature(options.spec);
|
|
22
|
+
if (recorded)
|
|
23
|
+
return finalization.status(recorded.feature);
|
|
19
24
|
return (await this.app.make(FeatureInspectionService)).status(options);
|
|
20
25
|
}
|
|
21
26
|
}
|
|
@@ -23,10 +23,11 @@ export function reviewSchema(root) {
|
|
|
23
23
|
return existing;
|
|
24
24
|
return program
|
|
25
25
|
.command("review")
|
|
26
|
-
.description("Require a specialist or freeze
|
|
26
|
+
.description("Require a specialist or freeze a ticket or feature inventory for review.")
|
|
27
27
|
.argument("[action]", "Use 'require' to pin a justified specialist reviewer")
|
|
28
28
|
.option("--spec <path>", "Feature spec path", CommandOptions.once("--spec"))
|
|
29
|
-
.option("--
|
|
29
|
+
.option("--ticket <id>", "Ticket ID", CommandOptions.once("--ticket"))
|
|
30
|
+
.option("--scope <scope>", "Workflow scope: delivery", CommandOptions.once("--scope"))
|
|
30
31
|
.option("--inventory <json>", "Controller-confirmed Git-derived inventory as a JSON string array", CommandOptions.once("--inventory"))
|
|
31
32
|
.option("--author <author>", "Controller author", CommandOptions.once("--author"))
|
|
32
33
|
.option("--type <name>", "Review-eligible Blueprint agent key to require", CommandOptions.once("--type"))
|
|
@@ -70,9 +70,9 @@ export class GitEvidence extends Service {
|
|
|
70
70
|
}
|
|
71
71
|
return paths;
|
|
72
72
|
}
|
|
73
|
-
worktreeContentDigest(paths) {
|
|
73
|
+
worktreeContentDigest(paths, workspace) {
|
|
74
74
|
const records = paths.map((path) => {
|
|
75
|
-
const location = join(this.app.workspaceRoot, path);
|
|
75
|
+
const location = join(workspace ?? this.app.workspaceRoot, path);
|
|
76
76
|
const entry = lstatIfPresent(location);
|
|
77
77
|
if (!entry)
|
|
78
78
|
return [path, "deleted", ""];
|
|
@@ -132,15 +132,17 @@ export class GitEvidence extends Service {
|
|
|
132
132
|
});
|
|
133
133
|
return createHash("sha256").update(JSON.stringify(records)).digest("hex");
|
|
134
134
|
}
|
|
135
|
-
|
|
135
|
+
cleanDispatchBaseline(feature, allowPendingRecords = false) {
|
|
136
136
|
const root = realpathSync(this.app.workspaceRoot);
|
|
137
|
+
if (allowPendingRecords)
|
|
138
|
+
return this.pendingDispatchBaseline(feature, root);
|
|
137
139
|
const changes = this.gitChangeSummary(feature);
|
|
138
140
|
if (changes) {
|
|
139
141
|
const deliveryRecords = this.deliveryRecordPaths(feature);
|
|
140
142
|
if (deliveryRecords.length > 0) {
|
|
141
|
-
throw new Error(`Cannot
|
|
143
|
+
throw new Error(`Cannot dispatch while uncommitted delivery records remain; inspect and confirm them before committing:\n${deliveryRecords.join("\n")}`);
|
|
142
144
|
}
|
|
143
|
-
throw new Error(`Cannot
|
|
145
|
+
throw new Error(`Cannot dispatch from a dirty checkout; inspect and resolve staged, unstaged, and unrelated changes first:\n${changes}`);
|
|
144
146
|
}
|
|
145
147
|
const revision = this.git.run(["rev-parse", "--verify", "HEAD"], {
|
|
146
148
|
cwd: root,
|
|
@@ -154,10 +156,90 @@ export class GitEvidence extends Service {
|
|
|
154
156
|
}
|
|
155
157
|
return revision.stdout.trim();
|
|
156
158
|
}
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
pendingDispatchBaseline(feature, root) {
|
|
160
|
+
for (const variable of [
|
|
161
|
+
"GIT_DIR",
|
|
162
|
+
"GIT_WORK_TREE",
|
|
163
|
+
"GIT_COMMON_DIR",
|
|
164
|
+
"GIT_INDEX_FILE",
|
|
165
|
+
"GIT_OBJECT_DIRECTORY",
|
|
166
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
167
|
+
"GIT_NAMESPACE",
|
|
168
|
+
]) {
|
|
169
|
+
if (process.env[variable])
|
|
170
|
+
throw new Error(`Unset ${variable} before ticket dispatch.`);
|
|
171
|
+
}
|
|
172
|
+
const ledger = this.ledgerRepository.readLedger(feature);
|
|
173
|
+
const pending = ledger.tickets
|
|
174
|
+
.map((ticket) => ticket.integration ?? ticket.integrationHistory?.at(-1))
|
|
175
|
+
.filter((integration) => integration?.postMerge?.status === "pending");
|
|
176
|
+
const head = this.headSha(root);
|
|
177
|
+
if (!pending.length || pending.some((integration) => integration?.mergeSha !== head)) {
|
|
178
|
+
throw new Error("Pending post-merge validation does not match the exact feature HEAD.");
|
|
179
|
+
}
|
|
180
|
+
const gitPath = (name) => resolve(root, this.gitBuffer(["rev-parse", "--git-path", name], root).toString("utf8").trim());
|
|
181
|
+
for (const operation of [
|
|
182
|
+
"MERGE_HEAD",
|
|
183
|
+
"CHERRY_PICK_HEAD",
|
|
184
|
+
"REVERT_HEAD",
|
|
185
|
+
"REBASE_HEAD",
|
|
186
|
+
"rebase-merge",
|
|
187
|
+
"rebase-apply",
|
|
188
|
+
"sequencer",
|
|
189
|
+
]) {
|
|
190
|
+
if (lstatIfPresent(gitPath(operation))) {
|
|
191
|
+
throw new Error(`Existing Git operation ${operation} was preserved; inspect it before dispatch.`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const common = realpathSync(this.gitBuffer(["rev-parse", "--path-format=absolute", "--git-common-dir"], root)
|
|
195
|
+
.toString("utf8")
|
|
196
|
+
.trim());
|
|
197
|
+
if (lstatIfPresent(join(common, "spindle-record-commit", `${feature.feature}.json`))) {
|
|
198
|
+
throw new Error("Pending workflow-record commit journal must be resolved before dispatch.");
|
|
199
|
+
}
|
|
200
|
+
const status = this.gitBuffer([
|
|
201
|
+
"status",
|
|
202
|
+
"--porcelain=v1",
|
|
203
|
+
"-z",
|
|
204
|
+
"--untracked-files=all",
|
|
205
|
+
"--ignore-submodules=none",
|
|
206
|
+
"--",
|
|
207
|
+
".",
|
|
208
|
+
], root)
|
|
209
|
+
.toString("utf8")
|
|
210
|
+
.split("\0")
|
|
211
|
+
.filter(Boolean);
|
|
212
|
+
const allowed = this.workflowDeliveryRecordPaths(feature, ledger);
|
|
213
|
+
const lock = `.scratch/${feature.feature}/.sdd-tickets.lock`;
|
|
214
|
+
for (const entry of status) {
|
|
215
|
+
const state = entry.slice(0, 2);
|
|
216
|
+
const path = entry.slice(3);
|
|
217
|
+
if (state[0] !== " " && state !== "??") {
|
|
218
|
+
throw new Error(`Staged data was preserved; inspect it before dispatch: ${path}.`);
|
|
219
|
+
}
|
|
220
|
+
if (path === lock)
|
|
221
|
+
continue;
|
|
222
|
+
if (!allowed.has(path)) {
|
|
223
|
+
throw new Error(`Unrelated or non-canonical changes were preserved: ${path}.`);
|
|
224
|
+
}
|
|
225
|
+
const file = lstatIfPresent(join(root, path));
|
|
226
|
+
if (!file?.isFile() || file.isSymbolicLink()) {
|
|
227
|
+
throw new Error(`Canonical workflow record is missing or unsafe: ${path}.`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (this.headSha(root) !== head)
|
|
231
|
+
throw new Error("Feature HEAD changed during ticket dispatch.");
|
|
232
|
+
return head;
|
|
233
|
+
}
|
|
234
|
+
contentIdentity(feature, ticket, baseSha, checks, coveredPaths, workspace) {
|
|
235
|
+
const progress = this.ledgerRepository
|
|
236
|
+
.readLedger(feature)
|
|
237
|
+
.tickets.find((item) => item.id === ticket.id);
|
|
238
|
+
const root = workspace ??
|
|
239
|
+
(progress ? this.ticketWorkspace(progress) : undefined) ??
|
|
240
|
+
realpathSync(this.app.workspaceRoot);
|
|
241
|
+
const headSha = this.headSha(root);
|
|
242
|
+
const changedPaths = this.changedPaths(baseSha ?? headSha, root);
|
|
161
243
|
const paths = new Set(coveredPaths ?? changedPaths);
|
|
162
244
|
if (coveredPaths) {
|
|
163
245
|
for (const path of paths) {
|
|
@@ -183,7 +265,7 @@ export class GitEvidence extends Service {
|
|
|
183
265
|
for (const path of [...paths].sort()) {
|
|
184
266
|
field("record", "path");
|
|
185
267
|
field("path", path);
|
|
186
|
-
field("index-stage", this.gitBuffer(["ls-files", "--stage", "-z", "--", `:(literal)${path}`]));
|
|
268
|
+
field("index-stage", this.gitBuffer(["ls-files", "--stage", "-z", "--", `:(literal)${path}`], root));
|
|
187
269
|
const indexed = this.git.run(["show", `:${path}`], {
|
|
188
270
|
cwd: root,
|
|
189
271
|
encoding: "buffer",
|
|
@@ -227,8 +309,16 @@ export class GitEvidence extends Service {
|
|
|
227
309
|
.update(JSON.stringify(requirementParts))
|
|
228
310
|
.digest("hex");
|
|
229
311
|
const snapshot = createHash("sha256")
|
|
230
|
-
.update(JSON.stringify(
|
|
312
|
+
.update(JSON.stringify(progress?.worktree && root === progress.worktree.workspace
|
|
313
|
+
? [codeSnapshot, requirementsSnapshot, headSha]
|
|
314
|
+
: [codeSnapshot, requirementsSnapshot]))
|
|
231
315
|
.digest("hex");
|
|
316
|
+
if (this.headSha(root) !== headSha) {
|
|
317
|
+
throw new Error("Git HEAD changed while computing content evidence.");
|
|
318
|
+
}
|
|
319
|
+
if (progress?.worktree && root === progress.worktree.workspace) {
|
|
320
|
+
this.ticketWorkspace(progress);
|
|
321
|
+
}
|
|
232
322
|
return { snapshot, codeSnapshot, requirementsSnapshot, headSha };
|
|
233
323
|
}
|
|
234
324
|
deliveryIdentity(feature, ledger, tickets) {
|
|
@@ -239,7 +329,7 @@ export class GitEvidence extends Service {
|
|
|
239
329
|
if (!delivery.baseSha) {
|
|
240
330
|
throw new Error("Aggregate evidence requires the first ticket's recorded Git baseline.");
|
|
241
331
|
}
|
|
242
|
-
const codeIdentity = this.contentIdentity(feature, tickets[0], delivery.baseSha);
|
|
332
|
+
const codeIdentity = this.contentIdentity(feature, tickets[0], delivery.baseSha, undefined, undefined, realpathSync(this.app.workspaceRoot));
|
|
243
333
|
const requirementsSnapshot = createHash("sha256")
|
|
244
334
|
.update(JSON.stringify([
|
|
245
335
|
readFileSync(feature.spec, "utf8"),
|
|
@@ -266,25 +356,25 @@ export class GitEvidence extends Service {
|
|
|
266
356
|
headSha: codeIdentity.headSha,
|
|
267
357
|
};
|
|
268
358
|
}
|
|
269
|
-
changedPaths(baseSha) {
|
|
359
|
+
changedPaths(baseSha, workspace) {
|
|
270
360
|
const paths = new Set();
|
|
271
361
|
for (const args of [
|
|
272
|
-
["diff", "--name-only", "-z", baseSha, "--", "."],
|
|
273
|
-
["diff", "--cached", "--name-only", "-z", baseSha, "--", "."],
|
|
362
|
+
["diff", "--name-only", "--no-renames", "-z", baseSha, "--", "."],
|
|
363
|
+
["diff", "--cached", "--name-only", "--no-renames", "-z", baseSha, "--", "."],
|
|
274
364
|
["ls-files", "--others", "--exclude-standard", "-z"],
|
|
275
365
|
]) {
|
|
276
|
-
const output = this.gitBuffer(args).toString("utf8");
|
|
366
|
+
const output = this.gitBuffer(args, workspace).toString("utf8");
|
|
277
367
|
for (const path of output.split("\0")) {
|
|
278
368
|
if (path && path !== ".scratch" && !path.startsWith(".scratch/"))
|
|
279
369
|
paths.add(path);
|
|
280
370
|
}
|
|
281
371
|
}
|
|
282
|
-
this.assertStableSubmodules(baseSha, paths);
|
|
372
|
+
this.assertStableSubmodules(baseSha, paths, workspace);
|
|
283
373
|
return [...paths].sort();
|
|
284
374
|
}
|
|
285
|
-
assertStableSubmodules(baseSha, changedPaths) {
|
|
375
|
+
assertStableSubmodules(baseSha, changedPaths, workspace) {
|
|
286
376
|
const gitlinks = new Map();
|
|
287
|
-
for (const record of this.gitBuffer(["ls-files", "--stage", "-z"])
|
|
377
|
+
for (const record of this.gitBuffer(["ls-files", "--stage", "-z"], workspace)
|
|
288
378
|
.toString("utf8")
|
|
289
379
|
.split("\0")) {
|
|
290
380
|
const [metadata, path] = record.split("\t", 2);
|
|
@@ -295,7 +385,7 @@ export class GitEvidence extends Service {
|
|
|
295
385
|
gitlinks.set(path, sha);
|
|
296
386
|
}
|
|
297
387
|
const knownPaths = new Set(gitlinks.keys());
|
|
298
|
-
for (const record of this.gitBuffer(["ls-tree", "-r", "-z", baseSha])
|
|
388
|
+
for (const record of this.gitBuffer(["ls-tree", "-r", "-z", baseSha], workspace)
|
|
299
389
|
.toString("utf8")
|
|
300
390
|
.split("\0")) {
|
|
301
391
|
const [metadata, path] = record.split("\t", 2);
|
|
@@ -307,7 +397,7 @@ export class GitEvidence extends Service {
|
|
|
307
397
|
}
|
|
308
398
|
}
|
|
309
399
|
for (const path of knownPaths) {
|
|
310
|
-
const entry = lstatIfPresent(join(this.app.workspaceRoot, path));
|
|
400
|
+
const entry = lstatIfPresent(join(workspace ?? this.app.workspaceRoot, path));
|
|
311
401
|
if (!entry) {
|
|
312
402
|
throw new Error(`Cannot compute code identity for submodule '${path}'; its worktree is missing or uninitialized.`);
|
|
313
403
|
}
|
|
@@ -321,21 +411,14 @@ export class GitEvidence extends Service {
|
|
|
321
411
|
continue;
|
|
322
412
|
let revision;
|
|
323
413
|
try {
|
|
324
|
-
revision = this.gitBuffer(["-C", path, "rev-parse", "--verify", "HEAD"])
|
|
414
|
+
revision = this.gitBuffer(["-C", path, "rev-parse", "--verify", "HEAD"], workspace)
|
|
325
415
|
.toString("utf8")
|
|
326
416
|
.trim();
|
|
327
417
|
}
|
|
328
418
|
catch {
|
|
329
419
|
throw new Error(`Cannot compute code identity for submodule '${path}'; its worktree is missing or uninitialized.`);
|
|
330
420
|
}
|
|
331
|
-
const status = this.gitBuffer([
|
|
332
|
-
"-C",
|
|
333
|
-
path,
|
|
334
|
-
"status",
|
|
335
|
-
"--porcelain",
|
|
336
|
-
"--untracked-files=all",
|
|
337
|
-
"--ignore-submodules=none",
|
|
338
|
-
]).toString("utf8");
|
|
421
|
+
const status = this.gitBuffer(["-C", path, "status", "--porcelain", "--untracked-files=all", "--ignore-submodules=none"], workspace).toString("utf8");
|
|
339
422
|
if (revision !== gitlinks.get(path) || status.length > 0) {
|
|
340
423
|
throw new Error(`Cannot compute code identity for changed submodule '${path}'; submodule changes are unsupported.`);
|
|
341
424
|
}
|
|
@@ -364,9 +447,75 @@ export class GitEvidence extends Service {
|
|
|
364
447
|
}
|
|
365
448
|
return JSON.stringify([...sections.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
|
366
449
|
}
|
|
367
|
-
|
|
450
|
+
ticketWorkspace(progress) {
|
|
451
|
+
if (!progress.worktree)
|
|
452
|
+
return undefined;
|
|
453
|
+
const recorded = progress.worktree;
|
|
454
|
+
const entry = lstatIfPresent(recorded.workspace);
|
|
455
|
+
if (!entry?.isDirectory() ||
|
|
456
|
+
entry.isSymbolicLink() ||
|
|
457
|
+
realpathSync(recorded.workspace) !== recorded.workspace) {
|
|
458
|
+
throw new Error(`Ticket ${progress.id} worktree is missing or unsafe: ${recorded.workspace}.`);
|
|
459
|
+
}
|
|
460
|
+
const canonical = realpathSync(this.app.workspaceRoot);
|
|
461
|
+
const common = this.gitBuffer(["rev-parse", "--path-format=absolute", "--git-common-dir"])
|
|
462
|
+
.toString("utf8")
|
|
463
|
+
.trim();
|
|
464
|
+
const ticketCommon = this.gitBuffer(["rev-parse", "--path-format=absolute", "--git-common-dir"], recorded.workspace)
|
|
465
|
+
.toString("utf8")
|
|
466
|
+
.trim();
|
|
467
|
+
const branch = this.gitBuffer(["symbolic-ref", "--quiet", "--short", "HEAD"], recorded.workspace)
|
|
468
|
+
.toString("utf8")
|
|
469
|
+
.trim();
|
|
470
|
+
const top = this.gitBuffer(["rev-parse", "--show-toplevel"], recorded.workspace)
|
|
471
|
+
.toString("utf8")
|
|
472
|
+
.trim();
|
|
473
|
+
const registrations = this.gitBuffer(["worktree", "list", "--porcelain", "-z"])
|
|
474
|
+
.toString("utf8")
|
|
475
|
+
.split("\0");
|
|
476
|
+
if (isPathInside(canonical, recorded.workspace) ||
|
|
477
|
+
realpathSync(common) !== realpathSync(ticketCommon) ||
|
|
478
|
+
realpathSync(top) !== recorded.workspace ||
|
|
479
|
+
!registrations.includes(`worktree ${recorded.workspace}`) ||
|
|
480
|
+
branch !== recorded.branch) {
|
|
481
|
+
throw new Error(`Ticket ${progress.id} worktree no longer matches its recorded repository and branch.`);
|
|
482
|
+
}
|
|
483
|
+
const ancestor = this.git.run(["merge-base", "--is-ancestor", recorded.baseSha, "HEAD"], {
|
|
484
|
+
cwd: recorded.workspace,
|
|
485
|
+
encoding: "utf8",
|
|
486
|
+
});
|
|
487
|
+
if (ancestor.error)
|
|
488
|
+
throw ancestor.error;
|
|
489
|
+
if (ancestor.status !== 0)
|
|
490
|
+
throw new Error(`Ticket ${progress.id} no longer descends from its recorded baseline.`);
|
|
491
|
+
return recorded.workspace;
|
|
492
|
+
}
|
|
493
|
+
assertTicketCodeCommitted(progress) {
|
|
494
|
+
const workspace = this.ticketWorkspace(progress);
|
|
495
|
+
if (!workspace)
|
|
496
|
+
return;
|
|
497
|
+
const scratchDiff = this.gitBuffer(["diff", "--name-only", "--no-renames", "-z", progress.baseSha, "HEAD", "--", ".scratch"], workspace);
|
|
498
|
+
const scratchStatus = this.gitBuffer(["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".scratch"], workspace);
|
|
499
|
+
if (scratchDiff.length || scratchStatus.length) {
|
|
500
|
+
throw new Error("Ticket worktree must not modify .scratch; use canonical feature records.");
|
|
501
|
+
}
|
|
502
|
+
const changed = new Set();
|
|
503
|
+
for (const args of [
|
|
504
|
+
["diff", "--name-only", "--no-renames", "-z", "HEAD", "--", "."],
|
|
505
|
+
["diff", "--cached", "--name-only", "--no-renames", "-z", "HEAD", "--", "."],
|
|
506
|
+
["ls-files", "--others", "--exclude-standard", "-z"],
|
|
507
|
+
]) {
|
|
508
|
+
for (const path of this.gitBuffer(args, workspace).toString("utf8").split("\0")) {
|
|
509
|
+
if (path && path !== ".scratch" && !path.startsWith(".scratch/"))
|
|
510
|
+
changed.add(path);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (changed.size > 0)
|
|
514
|
+
throw new Error(`Commit ticket code before review: ${[...changed].sort().join(", ")}.`);
|
|
515
|
+
}
|
|
516
|
+
headSha(workspace) {
|
|
368
517
|
const revision = this.git.run(["rev-parse", "--verify", "HEAD"], {
|
|
369
|
-
cwd: realpathSync(this.app.workspaceRoot),
|
|
518
|
+
cwd: workspace ?? realpathSync(this.app.workspaceRoot),
|
|
370
519
|
encoding: "utf8",
|
|
371
520
|
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
372
521
|
});
|
|
@@ -377,9 +526,9 @@ export class GitEvidence extends Service {
|
|
|
377
526
|
}
|
|
378
527
|
return revision.stdout.trim();
|
|
379
528
|
}
|
|
380
|
-
gitBuffer(args) {
|
|
529
|
+
gitBuffer(args, workspace) {
|
|
381
530
|
const result = this.git.run(args, {
|
|
382
|
-
cwd: realpathSync(this.app.workspaceRoot),
|
|
531
|
+
cwd: workspace ?? realpathSync(this.app.workspaceRoot),
|
|
383
532
|
encoding: "buffer",
|
|
384
533
|
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
385
534
|
});
|
|
@@ -411,7 +560,7 @@ export class GitEvidence extends Service {
|
|
|
411
560
|
if (status.error)
|
|
412
561
|
throw status.error;
|
|
413
562
|
if (status.status !== 0) {
|
|
414
|
-
throw new Error(`git status failed while checking
|
|
563
|
+
throw new Error(`git status failed while checking dispatch readiness: ${status.stderr.trim()}`);
|
|
415
564
|
}
|
|
416
565
|
return status.stdout.trim();
|
|
417
566
|
}
|
|
@@ -427,10 +576,12 @@ export class GitEvidence extends Service {
|
|
|
427
576
|
return false;
|
|
428
577
|
}
|
|
429
578
|
try {
|
|
579
|
+
const workspace = this.ticketWorkspace(progress);
|
|
430
580
|
return (progress.integration.contentDigest === accepted.contentDigest &&
|
|
431
581
|
JSON.stringify(progress.integration.files) === JSON.stringify(accepted.files) &&
|
|
432
|
-
JSON.stringify(this.changedPaths(progress.baseSha)) ===
|
|
433
|
-
|
|
582
|
+
JSON.stringify(this.changedPaths(progress.baseSha, workspace)) ===
|
|
583
|
+
JSON.stringify(accepted.files) &&
|
|
584
|
+
this.worktreeContentDigest(accepted.files, workspace) === accepted.contentDigest);
|
|
434
585
|
}
|
|
435
586
|
catch {
|
|
436
587
|
return false;
|
|
@@ -5,11 +5,9 @@ export const TICKET_CATEGORIES = ["bug", "enhancement"];
|
|
|
5
5
|
export const TRACKER_STATUSES = ["open", "in-progress", "closed"];
|
|
6
6
|
export const EXECUTION_STATES = [
|
|
7
7
|
"unstarted",
|
|
8
|
-
"claimed",
|
|
9
8
|
"implementing",
|
|
10
9
|
"reviewing",
|
|
11
10
|
"changes-requested",
|
|
12
|
-
"accepted",
|
|
13
11
|
"integrated",
|
|
14
12
|
"complete",
|
|
15
13
|
"blocked",
|
|
@@ -13,6 +13,10 @@ export function isImplementationAssignment(value) {
|
|
|
13
13
|
!value.controller.trim() ||
|
|
14
14
|
typeof value.report !== "string" ||
|
|
15
15
|
!isAbsolute(value.report) ||
|
|
16
|
+
(Object.hasOwn(value, "workspace") &&
|
|
17
|
+
(typeof value.workspace !== "string" || !isAbsolute(value.workspace))) ||
|
|
18
|
+
(Object.hasOwn(value, "branch") &&
|
|
19
|
+
(typeof value.branch !== "string" || !value.branch.trim())) ||
|
|
16
20
|
typeof value.requestedAt !== "string" ||
|
|
17
21
|
!Number.isFinite(Date.parse(value.requestedAt)) ||
|
|
18
22
|
!["pending", "submitted", "unavailable"].includes(String(value.status)) ||
|
|
@@ -12,7 +12,7 @@ export function isCheckRequirement(value) {
|
|
|
12
12
|
return false;
|
|
13
13
|
}
|
|
14
14
|
const removed = Object.hasOwn(value, "removedAt");
|
|
15
|
-
return (!removed ||
|
|
15
|
+
return ((!removed && !Object.hasOwn(value, "removedBy") && !Object.hasOwn(value, "removalDetails")) ||
|
|
16
16
|
(typeof value.removedAt === "string" &&
|
|
17
17
|
Number.isFinite(Date.parse(value.removedAt)) &&
|
|
18
18
|
typeof value.removedBy === "string" &&
|
|
@@ -24,7 +24,7 @@ export function isCheckRecord(value) {
|
|
|
24
24
|
if (isRecord(value) &&
|
|
25
25
|
typeof value.command === "string" &&
|
|
26
26
|
!!value.command.trim() &&
|
|
27
|
-
|
|
27
|
+
(value.result === "passed" || value.result === "failed" || value.result === "unavailable") &&
|
|
28
28
|
typeof value.author === "string" &&
|
|
29
29
|
!!value.author.trim() &&
|
|
30
30
|
typeof value.snapshot === "string" &&
|
|
@@ -37,6 +37,8 @@ export function isCheckRecord(value) {
|
|
|
37
37
|
/^[0-9a-f]{40,64}$/i.test(value.headSha) &&
|
|
38
38
|
typeof value.recordedAt === "string" &&
|
|
39
39
|
Number.isFinite(Date.parse(value.recordedAt)) &&
|
|
40
|
+
(!Object.hasOwn(value, "details") ||
|
|
41
|
+
(typeof value.details === "string" && !!value.details.trim())) &&
|
|
40
42
|
(value.result === "passed" || (typeof value.details === "string" && !!value.details.trim()))) {
|
|
41
43
|
const hasScope = Object.hasOwn(value, "coveredPaths") || Object.hasOwn(value, "coverageSnapshot");
|
|
42
44
|
return (!hasScope ||
|
|
@@ -49,17 +51,6 @@ export function isCheckRecord(value) {
|
|
|
49
51
|
}
|
|
50
52
|
return false;
|
|
51
53
|
}
|
|
52
|
-
export function isCheckGateInvalidation(value) {
|
|
53
|
-
return (isRecord(value) &&
|
|
54
|
-
typeof value.at === "string" &&
|
|
55
|
-
Number.isFinite(Date.parse(value.at)) &&
|
|
56
|
-
typeof value.author === "string" &&
|
|
57
|
-
!!value.author.trim() &&
|
|
58
|
-
typeof value.command === "string" &&
|
|
59
|
-
!!value.command.trim() &&
|
|
60
|
-
["accepted", "integrated"].includes(String(value.from)) &&
|
|
61
|
-
value.to === "reviewing");
|
|
62
|
-
}
|
|
63
54
|
export function isSafeCoveragePath(value) {
|
|
64
55
|
return (typeof value === "string" &&
|
|
65
56
|
!!value &&
|
|
@@ -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,
|
|
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
|
-
|
|
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`
|