@namewta/speculo 0.7.3 → 0.7.4
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/dist/src/migrations.js +190 -39
- package/dist/src/migrations.js.map +1 -1
- package/package.json +1 -1
- package/template/canonical/canonical-specdev-engineering-cognitive-mentor.md +51 -15
- package/template/canonical/canonical-specdev-goal-plan.md +183 -116
- package/template/canonical/canonical-specdev-grill-with-docs.md +51 -15
- package/template/canonical/canonical-specdev-spec.md +61 -21
- package/template/canonical/canonical-specdev-tickets.md +83 -43
- package/template/canonical/canonical-specdev-wayfinder.md +51 -15
- package/template/skills/migrate-runtime-state/references/migration-contract.md +3 -3
- package/template/skills/migrate-runtime-state/scripts/migrate-runtime-state.mjs +94 -12
- package/template/workflows/specdev/I-implement/I-implement.md +27 -26
- package/template/workflows/specdev/I-implement/evidence-template.md +18 -12
- package/template/workflows/specdev/I-implement/execution-preflight.md +11 -9
- package/template/workflows/specdev/I-init-setup/I-init-setup.md +2 -2
- package/template/workflows/specdev/I-init-setup/change-status-template.json +3 -3
- package/template/workflows/specdev/I-init-setup/config-template.json +5 -2
- package/template/workflows/specdev/INDEX.md +6 -6
- package/template/workflows/specdev/P-goal-plan/P-goal-plan.md +16 -16
- package/template/workflows/specdev/P-goal-plan/completion-control.md +8 -8
- package/template/workflows/specdev/P-goal-plan/goal-plan-template.md +20 -14
- package/template/workflows/specdev/P-goal-plan/lead-orchestration.md +5 -5
- package/template/workflows/specdev/P-goal-plan/orchestration-protocol.md +13 -13
- package/template/workflows/specdev/P-goal-plan/planning-modes.md +27 -15
- package/template/workflows/specdev/P-prototype/ui-prototype.md +1 -1
- package/template/workflows/specdev/T-tickets/T-tickets.md +4 -4
- package/template/workflows/specdev/T-tickets/ticket-readiness.md +3 -3
- package/template/workflows/specdev/T-tickets/ticket-template.md +6 -6
- package/template/workflows/specdev/T-tickets/tickets-map-template.md +2 -2
- package/template/workflows/specdev/common/README.md +1 -1
- package/template/workflows/specdev/common/rules/change-completion.md +2 -2
- package/template/workflows/specdev/common/rules/evidence-and-verification.md +10 -6
- package/template/workflows/specdev/common/rules/path-ownership.md +4 -4
- package/template/workflows/specdev/common/schemas/change-status.schema.json +31 -4
- package/template/workflows/specdev/common/schemas/config.schema.json +12 -6
- package/template/workflows/specdev/common/schemas/goal-plan.schema.json +15 -6
- package/template/workflows/specdev/common/skills/dev-worktree/SKILL.md +3 -3
- package/template/workflows/specdev/common/skills/dev-worktree/references/finalize.md +1 -1
- package/template/workflows/specdev/common/skills/subagent-delivery/SKILL.md +4 -4
- package/template/workflows/specdev/common/skills/subagent-delivery/references/external-web-subagent.md +1 -1
- package/template/workflows/specdev/common/skills/subagent-delivery/references/native-subagent.md +4 -4
- package/template/workflows/specdev/common/tools/README.md +2 -1
- package/template/workflows/specdev/common/tools/validate-specdev.mjs +261 -66
package/dist/src/migrations.js
CHANGED
|
@@ -6,6 +6,9 @@ const BACKUP_RELATIVE = ".speculo/back";
|
|
|
6
6
|
const INSTALL_RELATIVE = ".speculo/install.json";
|
|
7
7
|
const MIGRATION_RELATIVE = ".speculo/migration.json";
|
|
8
8
|
const SNAPSHOT_DIR = ".migration-snapshot";
|
|
9
|
+
const CONFIG_SCHEMA_VERSION = 5;
|
|
10
|
+
const GOAL_PLAN_SCHEMA_VERSION = 6;
|
|
11
|
+
const CHANGE_STATUS_SCHEMA_VERSION = 6;
|
|
9
12
|
const MANAGED_STATE_ENTRIES = new Set([
|
|
10
13
|
"README.md",
|
|
11
14
|
"workspace.json",
|
|
@@ -191,7 +194,7 @@ function hasCompleteV4Integration(integration, worktreeStatus, sourceCheckpoint,
|
|
|
191
194
|
return false;
|
|
192
195
|
if (!new Set(["pending", "candidate", "passed", "failed", "stale"]).has(String(integration.status)))
|
|
193
196
|
return false;
|
|
194
|
-
if (![null, "fast-forward", "merge-commit"].includes(integration.method))
|
|
197
|
+
if (![null, "direct-parent", "fast-forward", "merge-commit"].includes(integration.method))
|
|
195
198
|
return false;
|
|
196
199
|
if (!new Set(["pending", "passed", "failed"]).has(String(integration.verification)))
|
|
197
200
|
return false;
|
|
@@ -224,15 +227,18 @@ function hasCompleteV4Integration(integration, worktreeStatus, sourceCheckpoint,
|
|
|
224
227
|
return false;
|
|
225
228
|
if (e2e.required === true && e2e.status === "passed" && !isNonEmptyString(e2e.evidence))
|
|
226
229
|
return false;
|
|
230
|
+
const currentWorkspace = integration.method === "direct-parent";
|
|
227
231
|
const lifecycleNeedsCandidate = new Set(["integrating", "integrated", "removed"]).has(String(worktreeStatus));
|
|
228
232
|
if (lifecycleNeedsCandidate) {
|
|
229
233
|
if (!isNonEmptyString(integration.parent_before_sha) ||
|
|
230
234
|
!isNonEmptyString(integration.source_sha) ||
|
|
231
235
|
integration.source_sha !== sourceCheckpoint ||
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
+
(currentWorkspace
|
|
237
|
+
? integration.candidate_sha !== null || integration.candidate_branch !== null || integration.candidate_workspace_ref !== null || integration.method !== "direct-parent"
|
|
238
|
+
: !isNonEmptyString(integration.candidate_sha) ||
|
|
239
|
+
integration.candidate_branch !== `speculo/integration/${change}/${ticketId}` ||
|
|
240
|
+
integration.candidate_workspace_ref !== `specdev-worktree/.integration/${ticketId}` ||
|
|
241
|
+
!new Set(["fast-forward", "merge-commit"]).has(String(integration.method))) ||
|
|
236
242
|
!Number.isInteger(integration.attempts) ||
|
|
237
243
|
Number(integration.attempts) < 1)
|
|
238
244
|
return false;
|
|
@@ -243,7 +249,7 @@ function hasCompleteV4Integration(integration, worktreeStatus, sourceCheckpoint,
|
|
|
243
249
|
if (integration.status !== "passed" ||
|
|
244
250
|
integration.verification !== "passed" ||
|
|
245
251
|
!isNonEmptyString(integration.result_sha) ||
|
|
246
|
-
integration.result_sha !== integration.candidate_sha ||
|
|
252
|
+
integration.result_sha !== (currentWorkspace ? integration.source_sha : integration.candidate_sha) ||
|
|
247
253
|
!new Set(["not-required", "passed"]).has(String(e2e.status)))
|
|
248
254
|
return false;
|
|
249
255
|
if (integration.method === "fast-forward" &&
|
|
@@ -327,16 +333,17 @@ function hasCompleteV4ChangeStatus(status) {
|
|
|
327
333
|
if (!isNonEmptyString(entry[key]))
|
|
328
334
|
return false;
|
|
329
335
|
}
|
|
330
|
-
|
|
336
|
+
const integration = isJsonObject(entry.integration) ? entry.integration : {};
|
|
337
|
+
const currentWorkspace = entry.workspace_ref === "current" || integration.method === "direct-parent";
|
|
338
|
+
if (currentWorkspace ? entry.parent_branch !== entry.branch : entry.parent_branch === entry.branch)
|
|
331
339
|
return false;
|
|
332
|
-
if (entry.workspace_ref !== `specdev-worktree/${entry.ticket_id}`)
|
|
340
|
+
if (!currentWorkspace && entry.workspace_ref !== `specdev-worktree/${entry.ticket_id}`)
|
|
333
341
|
return false;
|
|
334
342
|
if (!new Set(["planned", "active", "review", "integrating", "integrated", "removed", "blocked"]).has(String(entry.status)))
|
|
335
343
|
return false;
|
|
336
344
|
const sourceRequired = new Set(["review", "integrating", "integrated", "removed"]).has(String(entry.status));
|
|
337
345
|
if (sourceRequired ? !isNonEmptyString(entry.source_checkpoint) : !isStringOrNull(entry.source_checkpoint))
|
|
338
346
|
return false;
|
|
339
|
-
const integration = entry.integration;
|
|
340
347
|
return isJsonObject(integration) && hasCompleteV4Integration(integration, entry.status, entry.source_checkpoint, String(status.change), entry.ticket_id);
|
|
341
348
|
});
|
|
342
349
|
}
|
|
@@ -357,7 +364,7 @@ function hasCompleteV4SpecdevConfig(config) {
|
|
|
357
364
|
if (!hasExactKeys(config.execution, ["max_implementation_agents", "deep_ticket_human_approval", "shared_path_owner"]))
|
|
358
365
|
return false;
|
|
359
366
|
const limit = config.execution.max_implementation_agents;
|
|
360
|
-
if (!Number.isInteger(limit) || Number(limit) < 1 ||
|
|
367
|
+
if (!Number.isInteger(limit) || Number(limit) < 1 ||
|
|
361
368
|
typeof config.execution.deep_ticket_human_approval !== "boolean" ||
|
|
362
369
|
!isNonEmptyString(config.execution.shared_path_owner))
|
|
363
370
|
return false;
|
|
@@ -369,6 +376,30 @@ function hasCompleteV4SpecdevConfig(config) {
|
|
|
369
376
|
typeof config.planning.require_ready_gate === "boolean" &&
|
|
370
377
|
typeof config.planning.require_evidence === "boolean";
|
|
371
378
|
}
|
|
379
|
+
function hasCompleteV5SpecdevConfig(config) {
|
|
380
|
+
const rootKeys = ["schema_version", "interaction_language", "artifact_language", "git", "execution", "verification", "planning"];
|
|
381
|
+
if (config.schema_version !== CONFIG_SCHEMA_VERSION || !hasExactKeys(config, rootKeys) ||
|
|
382
|
+
!isNonEmptyString(config.interaction_language) || !isNonEmptyString(config.artifact_language) ||
|
|
383
|
+
!isJsonObject(config.git) || !isJsonObject(config.execution) || !isJsonObject(config.verification) || !isJsonObject(config.planning))
|
|
384
|
+
return false;
|
|
385
|
+
if (!hasExactKeys(config.git, ["default_branch"]) || !(config.git.default_branch === null || typeof config.git.default_branch === "string"))
|
|
386
|
+
return false;
|
|
387
|
+
if (!hasExactKeys(config.execution, ["max_implementation_agents", "max_integration_attempts", "deep_ticket_human_approval", "shared_path_owner"]))
|
|
388
|
+
return false;
|
|
389
|
+
if (!Number.isInteger(config.execution.max_implementation_agents) || Number(config.execution.max_implementation_agents) < 1 ||
|
|
390
|
+
!Number.isInteger(config.execution.max_integration_attempts) || Number(config.execution.max_integration_attempts) < 1 ||
|
|
391
|
+
typeof config.execution.deep_ticket_human_approval !== "boolean" || !isNonEmptyString(config.execution.shared_path_owner))
|
|
392
|
+
return false;
|
|
393
|
+
for (const key of ["test", "typecheck", "lint", "build"]) {
|
|
394
|
+
if (!(key in config.verification) || !isStringOrNull(config.verification[key]))
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
return new Set(["lite", "standard", "deep"]).has(String(config.planning.default_depth)) &&
|
|
398
|
+
typeof config.planning.require_ready_gate === "boolean" &&
|
|
399
|
+
typeof config.planning.require_evidence === "boolean" &&
|
|
400
|
+
Number.isInteger(config.planning.ui_prototype_default_variants) && Number(config.planning.ui_prototype_default_variants) >= 1 &&
|
|
401
|
+
Number.isInteger(config.planning.ui_prototype_max_variants) && Number(config.planning.ui_prototype_max_variants) >= Number(config.planning.ui_prototype_default_variants);
|
|
402
|
+
}
|
|
372
403
|
function parseGoalPlanFrontmatter(text) {
|
|
373
404
|
const lines = text.split(/\r?\n/);
|
|
374
405
|
if (lines[0]?.trim() !== "---")
|
|
@@ -431,12 +462,35 @@ function hasCompleteV5GoalPlan(meta, change) {
|
|
|
431
462
|
return (new Set(["ready", "in_progress"]).has(status) && meta.ready_for_execution === true) ||
|
|
432
463
|
(new Set(["draft", "blocked", "completed"]).has(status) && meta.ready_for_execution === false);
|
|
433
464
|
}
|
|
465
|
+
function hasCompleteV6GoalPlan(meta, change) {
|
|
466
|
+
const required = [
|
|
467
|
+
"schema_version", "artifact", "change", "status", "modes", "orchestration", "lead",
|
|
468
|
+
"implementation_agent_limit", "integration_attempt_limit", "ticket_workspace_policy", "integration_gate", "ready_for_execution",
|
|
469
|
+
];
|
|
470
|
+
if (!hasExactKeys(meta, required) || meta.schema_version !== GOAL_PLAN_SCHEMA_VERSION)
|
|
471
|
+
return false;
|
|
472
|
+
const { integration_attempt_limit: integrationAttemptLimit, ...previous } = meta;
|
|
473
|
+
return hasCompleteV5GoalPlan({ ...previous, schema_version: 5 }, change) &&
|
|
474
|
+
Number.isInteger(integrationAttemptLimit) && Number(integrationAttemptLimit) >= 1;
|
|
475
|
+
}
|
|
434
476
|
function normalizeGoalPlanV5(text) {
|
|
435
477
|
const meta = parseGoalPlanFrontmatter(text);
|
|
436
478
|
if (meta === null || !hasCompleteV4GoalPlan(meta, String(meta.change)))
|
|
437
479
|
return text;
|
|
438
480
|
return text.replace(/^schema_version:\s*4\s*$/m, "schema_version: 5");
|
|
439
481
|
}
|
|
482
|
+
function normalizeGoalPlanV6(text, config) {
|
|
483
|
+
const meta = parseGoalPlanFrontmatter(text);
|
|
484
|
+
if (meta === null || meta.schema_version !== 5 || !hasCompleteV5GoalPlan(meta, String(meta.change)))
|
|
485
|
+
return text;
|
|
486
|
+
const execution = isJsonObject(config.execution) ? config.execution : {};
|
|
487
|
+
const attempts = Number.isInteger(execution.max_integration_attempts) && Number(execution.max_integration_attempts) >= 1
|
|
488
|
+
? Number(execution.max_integration_attempts)
|
|
489
|
+
: 3;
|
|
490
|
+
return text
|
|
491
|
+
.replace(/^schema_version:\s*5\s*$/m, "schema_version: 6")
|
|
492
|
+
.replace(/^(implementation_agent_limit:\s*[^\n]+)$/m, `$1\nintegration_attempt_limit: ${attempts}`);
|
|
493
|
+
}
|
|
440
494
|
function v5Authorization(scope) {
|
|
441
495
|
return { status: "not-authorized", source: null, granted_at: null, scope };
|
|
442
496
|
}
|
|
@@ -452,12 +506,21 @@ function normalizeChangeStatusV5(previous, globalEntry) {
|
|
|
452
506
|
const sourceCheckpoint = isStringOrNull(entry.source_checkpoint) ? entry.source_checkpoint : null;
|
|
453
507
|
return {
|
|
454
508
|
...entry,
|
|
455
|
-
workspace_ref:
|
|
509
|
+
workspace_ref: entry.workspace_ref === "current" || integration.method === "direct-parent"
|
|
510
|
+
? "current"
|
|
511
|
+
: `specdev-worktree/${change}/${ticketId}`,
|
|
512
|
+
branch: entry.workspace_ref === "current" || integration.method === "direct-parent"
|
|
513
|
+
? entry.parent_branch
|
|
514
|
+
: entry.branch,
|
|
456
515
|
integration: {
|
|
457
516
|
...integration,
|
|
458
517
|
parent_ref: typeof entry.parent_branch === "string" ? entry.parent_branch : null,
|
|
459
|
-
|
|
460
|
-
|
|
518
|
+
candidate_sha: entry.workspace_ref === "current" || integration.method === "direct-parent" ? null : integration.candidate_sha ?? null,
|
|
519
|
+
candidate_tree_sha: entry.workspace_ref === "current" || integration.method === "direct-parent" ? null : integration.candidate_sha ?? null,
|
|
520
|
+
candidate_branch: entry.workspace_ref === "current" || integration.method === "direct-parent" ? null : integration.candidate_branch ?? null,
|
|
521
|
+
candidate_workspace_ref: entry.workspace_ref === "current" || integration.method === "direct-parent"
|
|
522
|
+
? null
|
|
523
|
+
: integration.candidate_workspace_ref === null ? null : `specdev-worktree/.integration/${change}/${ticketId}`,
|
|
461
524
|
full_suite: { required: true, status: "pending", reason: null, evidence: null },
|
|
462
525
|
promotion_status: integration.status === "passed" ? "applied" : "pending",
|
|
463
526
|
source_sha: integration.source_sha ?? sourceCheckpoint,
|
|
@@ -474,8 +537,8 @@ function normalizeChangeStatusV5(previous, globalEntry) {
|
|
|
474
537
|
id: String(claim.id ?? ""), owner: String(claim.owner ?? ""), session: typeof claim.session === "string" ? claim.session : null, claimed_at: String(claim.claimed_at ?? ""),
|
|
475
538
|
} : claim) : [],
|
|
476
539
|
execution_authorization: {
|
|
477
|
-
implementation_commit: v5Authorization("Ticket
|
|
478
|
-
local_candidate_integration: v5Authorization("Lead-owned local parent candidate integration and parent update"),
|
|
540
|
+
implementation_commit: v5Authorization("Ticket implementation commits"),
|
|
541
|
+
local_candidate_integration: v5Authorization("Lead-owned local direct-parent or candidate integration and parent update"),
|
|
479
542
|
source_cleanup: v5Authorization("Source worktree and branch cleanup"),
|
|
480
543
|
},
|
|
481
544
|
leadership: { current: "unassigned", epoch: 1, assigned_at: String(previous.updated_at), history: [] },
|
|
@@ -502,8 +565,61 @@ function hasCompleteV5ChangeStatus(status) {
|
|
|
502
565
|
return false;
|
|
503
566
|
}
|
|
504
567
|
const leadership = status.leadership;
|
|
505
|
-
|
|
506
|
-
isNonEmptyString(leadership.assigned_at)
|
|
568
|
+
if (!isNonEmptyString(leadership.current) || !Number.isInteger(leadership.epoch) || Number(leadership.epoch) < 1 ||
|
|
569
|
+
!isNonEmptyString(leadership.assigned_at) || !Array.isArray(leadership.history) || !Array.isArray(status.worktrees))
|
|
570
|
+
return false;
|
|
571
|
+
const change = String(status.change);
|
|
572
|
+
const worktreeKeys = [
|
|
573
|
+
"ticket_id", "owner", "implementation_owner", "integration_owner", "provider", "base_sha",
|
|
574
|
+
"parent_branch", "branch", "workspace_ref", "source_checkpoint", "integration", "status", "updated_at",
|
|
575
|
+
];
|
|
576
|
+
const integrationKeys = [
|
|
577
|
+
"status", "parent_ref", "parent_before_sha", "source_sha", "candidate_sha", "candidate_tree_sha",
|
|
578
|
+
"candidate_branch", "candidate_workspace_ref", "result_sha", "method", "conflict_paths", "verification",
|
|
579
|
+
"full_suite", "e2e", "evidence", "attempts", "promotion_status",
|
|
580
|
+
];
|
|
581
|
+
return status.worktrees.every((entry) => {
|
|
582
|
+
if (!isJsonObject(entry) || !hasExactKeys(entry, worktreeKeys) || entry.provider !== "git" ||
|
|
583
|
+
typeof entry.ticket_id !== "string" || !/^T-[0-9]{2,}$/.test(entry.ticket_id) || !isJsonObject(entry.integration))
|
|
584
|
+
return false;
|
|
585
|
+
const current = entry.workspace_ref === "current";
|
|
586
|
+
if (current ? entry.branch !== entry.parent_branch : entry.workspace_ref !== `specdev-worktree/${change}/${entry.ticket_id}` || entry.branch === entry.parent_branch)
|
|
587
|
+
return false;
|
|
588
|
+
const integration = entry.integration;
|
|
589
|
+
if (!hasExactKeys(integration, integrationKeys) || !Number.isInteger(integration.attempts) || Number(integration.attempts) < 0 ||
|
|
590
|
+
!isStringArray(integration.conflict_paths) || !isNonEmptyString(integration.evidence))
|
|
591
|
+
return false;
|
|
592
|
+
for (const key of ["full_suite", "e2e"]) {
|
|
593
|
+
const suite = integration[key];
|
|
594
|
+
if (!isJsonObject(suite) || !hasExactKeys(suite, ["required", "status", "reason", "evidence"]) || typeof suite.required !== "boolean")
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
return true;
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
function hasCompleteV6ChangeStatus(status) {
|
|
601
|
+
if (status.schema_version !== CHANGE_STATUS_SCHEMA_VERSION)
|
|
602
|
+
return false;
|
|
603
|
+
const previous = { ...status, schema_version: 5 };
|
|
604
|
+
if (!hasCompleteV5ChangeStatus(previous) || !Array.isArray(status.worktrees))
|
|
605
|
+
return false;
|
|
606
|
+
return status.worktrees.every((entry) => {
|
|
607
|
+
if (!isJsonObject(entry) || !isJsonObject(entry.integration))
|
|
608
|
+
return false;
|
|
609
|
+
const current = entry.workspace_ref === "current";
|
|
610
|
+
if (current && entry.parent_branch !== entry.branch)
|
|
611
|
+
return false;
|
|
612
|
+
if (!current && entry.parent_branch === entry.branch)
|
|
613
|
+
return false;
|
|
614
|
+
const integration = entry.integration;
|
|
615
|
+
if (current && ["candidate_sha", "candidate_tree_sha", "candidate_branch", "candidate_workspace_ref"].some((key) => integration[key] !== null))
|
|
616
|
+
return false;
|
|
617
|
+
if (current && integration.method !== null && integration.method !== "direct-parent")
|
|
618
|
+
return false;
|
|
619
|
+
if (!current && integration.method === "direct-parent")
|
|
620
|
+
return false;
|
|
621
|
+
return true;
|
|
622
|
+
});
|
|
507
623
|
}
|
|
508
624
|
function hasCompleteV4GoalPlan(meta, change) {
|
|
509
625
|
const required = [
|
|
@@ -520,9 +636,10 @@ function hasCompleteV4GoalPlan(meta, change) {
|
|
|
520
636
|
!isNonEmptyString(meta.lead) ||
|
|
521
637
|
!Number.isInteger(meta.implementation_agent_limit) ||
|
|
522
638
|
Number(meta.implementation_agent_limit) < 1 ||
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
meta.integration_gate !== "
|
|
639
|
+
!new Set(["current", "required"]).has(String(meta.ticket_workspace_policy)) ||
|
|
640
|
+
!new Set(["direct-parent", "candidate-merge"]).has(String(meta.integration_gate)) ||
|
|
641
|
+
(meta.ticket_workspace_policy === "current" && meta.integration_gate !== "direct-parent") ||
|
|
642
|
+
(meta.ticket_workspace_policy === "required" && meta.integration_gate !== "candidate-merge") ||
|
|
526
643
|
typeof meta.ready_for_execution !== "boolean" ||
|
|
527
644
|
!Array.isArray(meta.modes))
|
|
528
645
|
return false;
|
|
@@ -535,12 +652,12 @@ async function inspectGoalPlanVersion(stateRoot, change, blockers) {
|
|
|
535
652
|
if (!(await pathExists(path)))
|
|
536
653
|
return;
|
|
537
654
|
const frontmatter = parseGoalPlanFrontmatter(await readFile(path, "utf8"));
|
|
538
|
-
const currentContract = frontmatter !== null && hasCompleteV5GoalPlan(frontmatter, change);
|
|
655
|
+
const currentContract = frontmatter !== null && (hasCompleteV6GoalPlan(frontmatter, change) || hasCompleteV5GoalPlan(frontmatter, change));
|
|
539
656
|
if (!currentContract) {
|
|
540
657
|
blockers.push({
|
|
541
658
|
code: "unsupported-goal-plan-contract",
|
|
542
659
|
path: `.speculo/specdev/changes/${change}/goal-plan.md`,
|
|
543
|
-
message: "Goal Plan must contain the complete
|
|
660
|
+
message: "Goal Plan must contain the complete Lead and workspace/integration v6 frontmatter contract",
|
|
544
661
|
});
|
|
545
662
|
}
|
|
546
663
|
}
|
|
@@ -582,11 +699,11 @@ async function inspectSpecdevState(stateRoot) {
|
|
|
582
699
|
else {
|
|
583
700
|
try {
|
|
584
701
|
const changeStatus = await readJson(changeStatusPath);
|
|
585
|
-
if (!new Set([3, 4, 5]).has(Number(changeStatus.schema_version)) ||
|
|
702
|
+
if (!new Set([3, 4, 5, 6]).has(Number(changeStatus.schema_version)) ||
|
|
586
703
|
changeStatus.artifact !== "change-status" ||
|
|
587
704
|
changeStatus.change !== name ||
|
|
588
705
|
!new Set(["active", "blocked", "completed"]).has(String(changeStatus.change_status))) {
|
|
589
|
-
blockers.push({ code: "unsupported-change-status", path: `.speculo/specdev/changes/${name}/.status.json`, message: "active change state must use schema v3/v4 and match its index entry" });
|
|
706
|
+
blockers.push({ code: "unsupported-change-status", path: `.speculo/specdev/changes/${name}/.status.json`, message: "active change state must use schema v3/v4/v5/v6 and match its index entry" });
|
|
590
707
|
}
|
|
591
708
|
else if (hasLegacyWorktreeState(changeStatus)) {
|
|
592
709
|
blockers.push({
|
|
@@ -616,6 +733,9 @@ async function inspectSpecdevState(stateRoot) {
|
|
|
616
733
|
message: "change-status v5 is missing required runtime authority fields",
|
|
617
734
|
});
|
|
618
735
|
}
|
|
736
|
+
else if (changeStatus.schema_version === 6 && !hasCompleteV6ChangeStatus(changeStatus)) {
|
|
737
|
+
blockers.push({ code: "invalid-change-status-v6", path: `.speculo/specdev/changes/${name}/.status.json`, message: "change-status v6 has inconsistent current/required workspace fields" });
|
|
738
|
+
}
|
|
619
739
|
await inspectGoalPlanVersion(stateRoot, name, blockers);
|
|
620
740
|
}
|
|
621
741
|
catch {
|
|
@@ -646,7 +766,7 @@ async function inspectSpecdevState(stateRoot) {
|
|
|
646
766
|
else {
|
|
647
767
|
try {
|
|
648
768
|
const archivedStatus = await readJson(archivedStatusPath);
|
|
649
|
-
if (!new Set([3, 4]).has(Number(archivedStatus.schema_version)) ||
|
|
769
|
+
if (!new Set([3, 4, 5, 6]).has(Number(archivedStatus.schema_version)) ||
|
|
650
770
|
archivedStatus.artifact !== "change-status" ||
|
|
651
771
|
archivedStatus.change !== item ||
|
|
652
772
|
archivedStatus.change_status !== "archived") {
|
|
@@ -673,6 +793,12 @@ async function inspectSpecdevState(stateRoot) {
|
|
|
673
793
|
message: "archived change-status v4 is incomplete",
|
|
674
794
|
});
|
|
675
795
|
}
|
|
796
|
+
else if (archivedStatus.schema_version === 5 && !hasCompleteV5ChangeStatus(archivedStatus)) {
|
|
797
|
+
blockers.push({ code: "invalid-archived-status-v5", path: `.speculo/specdev/archive/${month}/${item}/.status.json`, message: "archived change-status v5 is incomplete" });
|
|
798
|
+
}
|
|
799
|
+
else if (archivedStatus.schema_version === 6 && !hasCompleteV6ChangeStatus(archivedStatus)) {
|
|
800
|
+
blockers.push({ code: "invalid-archived-status-v6", path: `.speculo/specdev/archive/${month}/${item}/.status.json`, message: "archived change-status v6 has inconsistent current/required workspace fields" });
|
|
801
|
+
}
|
|
676
802
|
}
|
|
677
803
|
catch {
|
|
678
804
|
// The JSON tree check reports the parse failure with the precise path.
|
|
@@ -716,8 +842,8 @@ async function inspectSpecdevState(stateRoot) {
|
|
|
716
842
|
if (await pathExists(configPath)) {
|
|
717
843
|
try {
|
|
718
844
|
const config = await readJson(configPath);
|
|
719
|
-
if (!new Set([3, 4]).has(Number(config.schema_version))) {
|
|
720
|
-
blockers.push({ code: "unsupported-specdev-config", path: ".speculo/specdev/config.json", message: "automatic migration supports SpecDev config schema v3/v4 only" });
|
|
845
|
+
if (!new Set([3, 4, 5]).has(Number(config.schema_version))) {
|
|
846
|
+
blockers.push({ code: "unsupported-specdev-config", path: ".speculo/specdev/config.json", message: "automatic migration supports SpecDev config schema v3/v4/v5 only" });
|
|
721
847
|
}
|
|
722
848
|
else if (hasUnsupportedV3SpecdevConfigFields(config)) {
|
|
723
849
|
blockers.push({ code: "unmapped-specdev-config-fields", path: ".speculo/specdev/config.json", message: "SpecDev config v3 contains additional fields that require an explicit migration decision" });
|
|
@@ -725,6 +851,9 @@ async function inspectSpecdevState(stateRoot) {
|
|
|
725
851
|
else if (config.schema_version === 4 && !hasCompleteV4SpecdevConfig(config)) {
|
|
726
852
|
blockers.push({ code: "invalid-specdev-config-v4", path: ".speculo/specdev/config.json", message: "SpecDev config v4 is incomplete or contains legacy execution fields" });
|
|
727
853
|
}
|
|
854
|
+
else if (config.schema_version === 5 && !hasCompleteV5SpecdevConfig(config)) {
|
|
855
|
+
blockers.push({ code: "invalid-specdev-config-v5", path: ".speculo/specdev/config.json", message: "SpecDev config v5 is incomplete or contains invalid execution limits" });
|
|
856
|
+
}
|
|
728
857
|
}
|
|
729
858
|
catch {
|
|
730
859
|
// The JSON tree check reports the parse failure with the precise path.
|
|
@@ -838,19 +967,24 @@ function mergeDefaults(defaults, previous) {
|
|
|
838
967
|
}
|
|
839
968
|
return previous;
|
|
840
969
|
}
|
|
841
|
-
function
|
|
842
|
-
if (previous.schema_version ===
|
|
970
|
+
function normalizeSpecdevConfigV5(previous, defaults) {
|
|
971
|
+
if (previous.schema_version === CONFIG_SCHEMA_VERSION)
|
|
843
972
|
return previous;
|
|
844
973
|
const previousGit = isJsonObject(previous.git) ? previous.git : {};
|
|
845
974
|
const previousExecution = isJsonObject(previous.execution) ? previous.execution : {};
|
|
846
975
|
const defaultGit = isJsonObject(defaults.git) ? defaults.git : {};
|
|
847
976
|
const defaultExecution = isJsonObject(defaults.execution) ? defaults.execution : {};
|
|
977
|
+
const configuredLimit = Number(previousExecution.max_implementation_agents);
|
|
848
978
|
const legacyLimit = Number(previousExecution.max_parallel);
|
|
849
|
-
const maxImplementationAgents = Number.isInteger(
|
|
850
|
-
?
|
|
851
|
-
: Number(
|
|
979
|
+
const maxImplementationAgents = Number.isInteger(configuredLimit) && configuredLimit >= 1
|
|
980
|
+
? configuredLimit
|
|
981
|
+
: Number.isInteger(legacyLimit) && legacyLimit >= 1
|
|
982
|
+
? legacyLimit
|
|
983
|
+
: Number(defaultExecution.max_implementation_agents ?? 3);
|
|
984
|
+
const merged = mergeDefaults(defaults, previous);
|
|
852
985
|
return {
|
|
853
|
-
|
|
986
|
+
...merged,
|
|
987
|
+
schema_version: CONFIG_SCHEMA_VERSION,
|
|
854
988
|
interaction_language: typeof previous.interaction_language === "string"
|
|
855
989
|
? previous.interaction_language
|
|
856
990
|
: defaults.interaction_language,
|
|
@@ -864,6 +998,9 @@ function normalizeSpecdevConfigV4(previous, defaults) {
|
|
|
864
998
|
},
|
|
865
999
|
execution: {
|
|
866
1000
|
max_implementation_agents: maxImplementationAgents,
|
|
1001
|
+
max_integration_attempts: Number.isInteger(previousExecution.max_integration_attempts) && Number(previousExecution.max_integration_attempts) >= 1
|
|
1002
|
+
? previousExecution.max_integration_attempts
|
|
1003
|
+
: Number(defaultExecution.max_integration_attempts ?? 3),
|
|
867
1004
|
deep_ticket_human_approval: typeof previousExecution.deep_ticket_human_approval === "boolean"
|
|
868
1005
|
? previousExecution.deep_ticket_human_approval
|
|
869
1006
|
: defaultExecution.deep_ticket_human_approval,
|
|
@@ -917,9 +1054,9 @@ async function upgradeSpecdevRuntimeV5(stagedRoot) {
|
|
|
917
1054
|
const configPath = join(stateRoot, "config.json");
|
|
918
1055
|
if (await pathExists(configPath)) {
|
|
919
1056
|
const previous = await readJson(configPath);
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
await writeFile(configPath, JSON.stringify(
|
|
1057
|
+
const defaults = await readJson(join(stagedRoot, "workflows", "specdev", "I-init-setup", "config-template.json"));
|
|
1058
|
+
if (previous.schema_version !== CONFIG_SCHEMA_VERSION) {
|
|
1059
|
+
await writeFile(configPath, JSON.stringify(normalizeSpecdevConfigV5(previous, defaults), null, 2) + "\n", "utf8");
|
|
923
1060
|
}
|
|
924
1061
|
}
|
|
925
1062
|
const statusPath = join(stateRoot, "status.json");
|
|
@@ -942,10 +1079,24 @@ async function upgradeSpecdevRuntimeV5(stagedRoot) {
|
|
|
942
1079
|
const previous = await readJson(path);
|
|
943
1080
|
if (previous.schema_version === 3) {
|
|
944
1081
|
await writeFile(path, JSON.stringify(normalizeChangeStatusV4(previous), null, 2) + "\n", "utf8");
|
|
945
|
-
continue;
|
|
946
1082
|
}
|
|
947
|
-
|
|
948
|
-
|
|
1083
|
+
let current = await readJson(path);
|
|
1084
|
+
if (current.schema_version === 4) {
|
|
1085
|
+
await writeFile(path, JSON.stringify(normalizeChangeStatusV5(current, entry), null, 2) + "\n", "utf8");
|
|
1086
|
+
}
|
|
1087
|
+
current = await readJson(path);
|
|
1088
|
+
if (current.schema_version === 5) {
|
|
1089
|
+
await writeFile(path, JSON.stringify({ ...current, schema_version: CHANGE_STATUS_SCHEMA_VERSION }, null, 2) + "\n", "utf8");
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
const config = await pathExists(configPath) ? await readJson(configPath) : {};
|
|
1093
|
+
for (const { path } of changeStatusPaths) {
|
|
1094
|
+
const goalPlanPath = join(dirname(path), "goal-plan.md");
|
|
1095
|
+
if (!(await pathExists(goalPlanPath)))
|
|
1096
|
+
continue;
|
|
1097
|
+
const goalPlan = await readFile(goalPlanPath, "utf8");
|
|
1098
|
+
if (parseGoalPlanFrontmatter(goalPlan)?.schema_version === 5) {
|
|
1099
|
+
await writeFile(goalPlanPath, normalizeGoalPlanV6(goalPlan, config), "utf8");
|
|
949
1100
|
}
|
|
950
1101
|
}
|
|
951
1102
|
if (status.schema_version === 4) {
|