phasegate 0.340.0 → 0.341.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/docs/folder_management_rules.md +10 -1
  3. package/docs/guide/cli-reference.md +23 -3
  4. package/docs/guide/configuration.md +28 -2
  5. package/docs/guide/hooks-integration.md +7 -1
  6. package/docs/guide/skills-overview.md +3 -0
  7. package/package.json +2 -1
  8. package/scripts/harness/agent-integration/application/usecases/handle-post-tool-use-usecase.ts +4 -0
  9. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +36 -1
  10. package/scripts/harness/agent-integration/application/usecases/handle-stop-usecase.ts +3 -9
  11. package/scripts/harness/agent-integration/domain/ports/story-reflection-query-port.ts +2 -0
  12. package/scripts/harness/agent-integration/domain/services/hook-to-cli-translator.ts +8 -5
  13. package/scripts/harness/agent-integration/domain/value-objects/hook-translation-result.ts +1 -1
  14. package/scripts/harness/agent-integration/domain/value-objects/story-reflection-query-result.ts +10 -2
  15. package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +12 -0
  16. package/scripts/harness/agent-integration/infrastructure/adapters/child-process-cli-executor-adapter.ts +64 -13
  17. package/scripts/harness/agent-integration/infrastructure/adapters/file-system-story-reflection-query-adapter.ts +108 -2
  18. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +8 -6
  19. package/scripts/harness/agent-integration/presentation/hook-skip-event-recorder.ts +3 -0
  20. package/scripts/harness/agent-integration/presentation/post-tool-use-feedback.ts +39 -0
  21. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +20 -27
  22. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +3 -0
  23. package/scripts/harness/agent-integration/presentation/stop-hook.ts +1 -3
  24. package/scripts/harness/biome-ast-engine/infrastructure/adapters/biome-cli-executor-adapter.ts +21 -1
  25. package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +5 -0
  26. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +15 -0
  27. package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts +6 -3
  28. package/scripts/harness/harness-api/domain/ports/biome-lint-port.ts +2 -1
  29. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +12 -1
  30. package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +10 -2
  31. package/scripts/harness/installation/application/bundled-skill-selection.ts +28 -2
  32. package/scripts/harness/installation/application/checks/check-utils.ts +2 -2
  33. package/scripts/harness/installation/application/usecases/run-install.ts +5 -5
  34. package/scripts/harness/installation/application/usecases/run-reconcile.ts +62 -21
  35. package/scripts/harness/installation/presentation/cli/install-handler.ts +2 -1
  36. package/scripts/harness/main.ts +128 -90
  37. package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +48 -5
  38. package/scripts/harness/phase-dependency-model/domain/services/work-item-reflection-scope-resolver.ts +64 -0
  39. package/scripts/harness/phase-dependency-model/domain/values/story-reflection-mapping.ts +8 -2
  40. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +6 -1
  41. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-work-item-dependency-catalog.ts +73 -0
  42. package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts +4 -0
  43. package/scripts/harness/quick-mode/application/ports/change-risk-advisory-port.ts +16 -0
  44. package/scripts/harness/quick-mode/composition-root.ts +5 -1
  45. package/scripts/harness/quick-mode/infrastructure/adapters/snapshot-risk-advisory-adapter.ts +118 -0
  46. package/scripts/harness/quick-mode/presentation/formatters/change-category-formatter.ts +7 -0
  47. package/scripts/harness/quick-mode/presentation/handlers/check-change-category-handler.ts +12 -1
  48. package/scripts/harness/setup/skill-deployer.ts +3 -45
  49. package/scripts/harness/skill-quality/application/dto/run-plan-checker-loop-output.ts +1 -0
  50. package/scripts/harness/skill-quality/application/usecases/apply-cascade-update-usecase.ts +9 -4
  51. package/scripts/harness/skill-quality/application/usecases/execute-tdd-cycle-usecase.ts +2 -1
  52. package/scripts/harness/skill-quality/application/usecases/run-plan-checker-loop-usecase.ts +8 -0
  53. package/scripts/harness/skill-quality/composition-root.ts +14 -7
  54. package/scripts/harness/skill-quality/domain/ports/plan-check-executor-port.ts +2 -0
  55. package/scripts/harness/skill-quality/domain/value-objects/cascade-update-target.ts +15 -1
  56. package/scripts/harness/skill-quality/infrastructure/adapters/l1-biome-validator-adapter.ts +9 -1
  57. package/scripts/harness/skill-quality/infrastructure/adapters/l2-validator-system-adapter.ts +18 -2
  58. package/scripts/harness/skill-quality/presentation/handlers/apply-cascade-update-handler.ts +8 -2
  59. package/scripts/harness/skill-quality/presentation/handlers/execute-tdd-cycle-handler.ts +4 -3
  60. package/scripts/harness/skill-quality/presentation/handlers/run-plan-checker-loop-handler.ts +11 -4
  61. package/scripts/harness/traceability-model/infrastructure/parsers/work-item-frontmatter-parser.ts +41 -0
  62. package/skills/README.md +7 -0
  63. package/skills/cascade-updater/SKILL.md +37 -109
  64. package/skills/cascade-updater/references//345/261/244/345/210/245/345/210/244/345/256/232/343/202/254/343/202/244/343/203/211.md +5 -3
  65. package/skills/release-publisher/SKILL.md +3 -1
  66. package/skills/skill-creator/SKILL.md +8 -9
@@ -86,7 +86,7 @@ export interface RunInstallInput {
86
86
  readonly includeCodex?: boolean;
87
87
  readonly includeHusky?: boolean;
88
88
  readonly includeCi?: boolean;
89
- readonly skillSet?: "core" | "all";
89
+ readonly skillSet?: SkillSet;
90
90
  readonly workflow?: "standard" | "strict";
91
91
  readonly agent?: AgentTarget;
92
92
  readonly personal?: boolean;
@@ -368,7 +368,7 @@ function renderAgentContextTemplate(
368
368
  template: string,
369
369
  options: {
370
370
  readonly agent: AgentTarget;
371
- readonly skillSet: "core" | "all";
371
+ readonly skillSet: SkillSet;
372
372
  readonly workflow: "standard" | "strict";
373
373
  readonly includeHusky: boolean;
374
374
  readonly includeCi: boolean;
@@ -388,7 +388,7 @@ function renderAgentContextTemplate(
388
388
  .replaceAll("{{PHASEGATE_HUSKY_STATE}}", options.includeHusky ? "managed" : "not managed by this setup run")
389
389
  .replaceAll("{{PHASEGATE_CI_STATE}}", options.includeCi ? "managed" : "not managed by this setup run")
390
390
  .replaceAll("{{PHASEGATE_COMMANDS}}", commands)
391
- .replaceAll("{{PHASEGATE_SKILLS}}", options.skillSet === "core" ? "- `core skills`" : "- `all bundled skills`")
391
+ .replaceAll("{{PHASEGATE_SKILLS}}", options.skillSet === "all" ? "- `all bundled skills`" : `- \`${options.skillSet} skills\``)
392
392
  .replaceAll("{{PHASEGATE_PRESETS}}", "- `minimal`\n- `standard`\n- `full`\n- `custom`")
393
393
  .replaceAll("{{PHASEGATE_USER_SECTION}}", USER_SECTION_PLACEHOLDER);
394
394
  }
@@ -1146,13 +1146,13 @@ export class RunInstallUseCase {
1146
1146
  private personalSkillsVersionHashInput(
1147
1147
  path: string,
1148
1148
  version: string,
1149
- skillSet: "core" | "all",
1149
+ skillSet: SkillSet,
1150
1150
  skills: readonly string[],
1151
1151
  ): string {
1152
1152
  return `personal-skills-version:${path}:${version}:${skillSet}:${skills.join(",")}`;
1153
1153
  }
1154
1154
 
1155
- private personalSkillHashInput(path: string, skill: string, version: string, skillSet: "core" | "all"): string {
1155
+ private personalSkillHashInput(path: string, skill: string, version: string, skillSet: SkillSet): string {
1156
1156
  return `personal-skill:${path}:${skill}:${version}:${skillSet}`;
1157
1157
  }
1158
1158
 
@@ -12,6 +12,7 @@
12
12
  // @work-item-id WI-331
13
13
  // @work-item-id WI-385
14
14
  // @work-item-id WI-387
15
+ // @work-item-id WI-220
15
16
 
16
17
  import {
17
18
  access,
@@ -31,7 +32,7 @@ import { DeploymentEntry } from "../../domain/deployment-entry.js";
31
32
  import { DeploymentManifest } from "../../domain/deployment-manifest.js";
32
33
  import type { ManagedBlockInput } from "../../domain/managed-block.js";
33
34
  import type { RepairMode } from "../../domain/repair-mode.js";
34
- import { getBundledSkillsForSet, type SkillSet } from "../bundled-skill-selection.js";
35
+ import { getBundledSkillsForSet, resolveInstalledSkillSet, type SkillSet } from "../bundled-skill-selection.js";
35
36
  import { mergeNamedHookJson } from "../named-hook-json.js";
36
37
  import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
37
38
  import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
@@ -327,7 +328,7 @@ function reconcileManagedMarkdown(existing: string | null, incoming: string): st
327
328
  return next.replace(/\s*$/, "\n");
328
329
  }
329
330
 
330
- function renderAgentContextTemplate(template: string): string {
331
+ function renderAgentContextTemplate(template: string, skillSet: SkillSet): string {
331
332
  const commands = [
332
333
  "phasegate doctor",
333
334
  "phasegate phasegate:check-ready",
@@ -339,12 +340,12 @@ function renderAgentContextTemplate(template: string): string {
339
340
  .join("\n");
340
341
  return template
341
342
  .replaceAll("{{PHASEGATE_AGENT}}", "both")
342
- .replaceAll("{{PHASEGATE_SKILLS_MODE}}", "all")
343
+ .replaceAll("{{PHASEGATE_SKILLS_MODE}}", skillSet)
343
344
  .replaceAll("{{PHASEGATE_WORKFLOW}}", "standard")
344
345
  .replaceAll("{{PHASEGATE_HUSKY_STATE}}", "managed")
345
346
  .replaceAll("{{PHASEGATE_CI_STATE}}", "managed")
346
347
  .replaceAll("{{PHASEGATE_COMMANDS}}", commands)
347
- .replaceAll("{{PHASEGATE_SKILLS}}", "- `all bundled skills`")
348
+ .replaceAll("{{PHASEGATE_SKILLS}}", skillSet === "all" ? "- `all bundled skills`" : `- \`${skillSet} skills\``)
348
349
  .replaceAll("{{PHASEGATE_PRESETS}}", "- `minimal`\n- `standard`\n- `full`\n- `custom`")
349
350
  .replaceAll("{{PHASEGATE_USER_SECTION}}", USER_SECTION_PLACEHOLDER);
350
351
  }
@@ -407,11 +408,13 @@ export class RunReconcileUseCase {
407
408
  const changed: ReconcilePlanItem[] = [];
408
409
  const backupStamp = `reconcile-${new Date().toISOString().replace(/[:.]/g, "-")}`;
409
410
  let backupDir: string | null = null;
411
+ let repairedMetadata = false;
410
412
 
411
413
  const outcomes: Array<{
412
414
  readonly item: ReconcilePlanItem;
413
415
  readonly needsBackup: boolean;
414
416
  readonly prune?: boolean;
417
+ readonly recoveredContent?: string;
415
418
  readonly apply: () => Promise<string | null>;
416
419
  }> = [];
417
420
 
@@ -431,7 +434,7 @@ export class RunReconcileUseCase {
431
434
  plan.push(item);
432
435
  continue;
433
436
  }
434
- const outcome = await this.planManagedEntry(input, entry, target);
437
+ const outcome = await this.planManagedEntry(input, entry, target, manifest.version);
435
438
  outcomes.push(outcome);
436
439
  plan.push(outcome.item);
437
440
  if (
@@ -460,16 +463,22 @@ export class RunReconcileUseCase {
460
463
  }
461
464
 
462
465
  const personalInstall = this.isPersonalManifest(manifest);
466
+ const skillSets = new Map<string, SkillSet>();
467
+ for (const root of personalInstall ? this.personalSkillPaths(manifest) : ["skills"]) {
468
+ skillSets.set(root, resolveInstalledSkillSet(await readTextOrNull(join(input.projectRoot, root, HARNESS_VERSION_BASENAME))));
469
+ }
463
470
  if (!personalInstall && this.manifestIntendsSharedSkills(manifest)) {
464
- const sharedSkills = await listSelectedBundledSkills(input.harnessRoot, "all");
465
- const outcome = await this.planSharedSkills(input, manifest, sharedSkills, "all");
471
+ const skillSet = skillSets.get("skills") ?? "all";
472
+ const sharedSkills = await listSelectedBundledSkills(input.harnessRoot, skillSet);
473
+ const outcome = await this.planSharedSkills(input, manifest, sharedSkills, skillSet);
466
474
  outcomes.push(outcome);
467
475
  plan.push(outcome.item);
468
476
  }
469
477
  if (personalInstall) {
470
- const personalSkills = await listSelectedBundledSkills(input.harnessRoot, "all");
471
478
  for (const skillPath of this.personalSkillPaths(manifest)) {
472
- const outcome = await this.planPersonalSkills(input, manifest, skillPath, personalSkills, "all");
479
+ const skillSet = skillSets.get(skillPath) ?? "all";
480
+ const personalSkills = await listSelectedBundledSkills(input.harnessRoot, skillSet);
481
+ const outcome = await this.planPersonalSkills(input, manifest, skillPath, personalSkills, skillSet);
473
482
  outcomes.push(outcome);
474
483
  plan.push(outcome.item);
475
484
  }
@@ -485,7 +494,17 @@ export class RunReconcileUseCase {
485
494
  }
486
495
 
487
496
  for (const outcome of outcomes) {
488
- if (!outcome.item.changed) continue;
497
+ if (!outcome.item.changed) {
498
+ const previous = nextManifest.findEntry(outcome.item.path);
499
+ if (previous !== null && outcome.recoveredContent !== undefined) {
500
+ nextManifest = nextManifest.addEntry(DeploymentEntry.create({
501
+ ...previous,
502
+ hash: this.hashCalculator.compute(outcome.recoveredContent),
503
+ }));
504
+ repairedMetadata = true;
505
+ }
506
+ continue;
507
+ }
489
508
  if (outcome.prune === true) {
490
509
  await outcome.apply();
491
510
  nextManifest = nextManifest.removeEntry(outcome.item.path);
@@ -500,16 +519,17 @@ export class RunReconcileUseCase {
500
519
  changed.push(outcome.item);
501
520
  if (hashContent !== null) {
502
521
  if (outcome.item.strategy === "copy-dir" && outcome.item.path === "skills") {
503
- const sharedSkills = await listSelectedBundledSkills(input.harnessRoot, "all");
522
+ const skillSet = skillSets.get("skills") ?? "all";
523
+ const sharedSkills = await listSelectedBundledSkills(input.harnessRoot, skillSet);
504
524
  nextManifest = nextManifest.addEntry(
505
525
  this.createdEntry(
506
526
  SHARED_SKILLS_VERSION_PATH,
507
- this.sharedSkillsVersionHashInput(input.phasegateVersion, "all", sharedSkills),
527
+ this.sharedSkillsVersionHashInput(input.phasegateVersion, skillSet, sharedSkills),
508
528
  ),
509
529
  );
510
530
  for (const skill of sharedSkills) {
511
531
  nextManifest = nextManifest.addEntry(
512
- this.createdEntry(`skills/${skill}`, this.sharedSkillHashInput(skill, input.phasegateVersion, "all")),
532
+ this.createdEntry(`skills/${skill}`, this.sharedSkillHashInput(skill, input.phasegateVersion, skillSet)),
513
533
  );
514
534
  }
515
535
  } else if (
@@ -518,18 +538,19 @@ export class RunReconcileUseCase {
518
538
  outcome.item.path === ".codex/skills" ||
519
539
  outcome.item.path === ".agents/skills")
520
540
  ) {
521
- const personalSkills = await listSelectedBundledSkills(input.harnessRoot, "all");
541
+ const skillSet = skillSets.get(outcome.item.path) ?? "all";
542
+ const personalSkills = await listSelectedBundledSkills(input.harnessRoot, skillSet);
522
543
  nextManifest = nextManifest.addEntry(
523
544
  this.createdEntry(
524
545
  `${outcome.item.path}/.harness-version`,
525
- this.personalSkillsVersionHashInput(outcome.item.path, input.phasegateVersion, "all", personalSkills),
546
+ this.personalSkillsVersionHashInput(outcome.item.path, input.phasegateVersion, skillSet, personalSkills),
526
547
  ),
527
548
  );
528
549
  for (const skill of personalSkills) {
529
550
  nextManifest = nextManifest.addEntry(
530
551
  this.createdEntry(
531
552
  `${outcome.item.path}/${skill}`,
532
- this.personalSkillHashInput(outcome.item.path, skill, input.phasegateVersion, "all"),
553
+ this.personalSkillHashInput(outcome.item.path, skill, input.phasegateVersion, skillSet),
533
554
  ),
534
555
  );
535
556
  }
@@ -553,13 +574,13 @@ export class RunReconcileUseCase {
553
574
  }
554
575
  }
555
576
 
556
- if (changed.length > 0 || manifest.version !== input.phasegateVersion) {
577
+ if (changed.length > 0 || repairedMetadata || manifest.version !== input.phasegateVersion) {
557
578
  await this.manifestRepository.save(input.projectRoot, nextManifest);
558
579
  }
559
580
  return { plan, refused, changed, backupDir };
560
581
  }
561
582
 
562
- private async planManagedEntry(input: RunReconcileInput, entry: DeploymentEntry, target: ReconcileTarget) {
583
+ private async planManagedEntry(input: RunReconcileInput, entry: DeploymentEntry, target: ReconcileTarget, previousVersion: string) {
563
584
  if (target.strategy === "symlink") return this.planSymlink(input.projectRoot, target.path);
564
585
  const absolutePath = this.resolveProjectPath(input.projectRoot, entry.path);
565
586
  const before = await readTextOrNull(absolutePath);
@@ -567,7 +588,8 @@ export class RunReconcileUseCase {
567
588
  const currentHash = this.hashCalculator.compute(before);
568
589
  const matchesManifest = currentHash.equals(entry.hash);
569
590
  const rawTemplate = target.templatePath ? await readFile(join(input.harnessRoot, target.templatePath), "utf8") : "";
570
- const template = target.strategy === "markdown-managed" ? renderAgentContextTemplate(rawTemplate) : rawTemplate;
591
+ const template = target.strategy === "markdown-managed"
592
+ ? renderAgentContextTemplate(rawTemplate, await this.contextSkillSet(input.projectRoot, target.path)) : rawTemplate;
571
593
  const next =
572
594
  entry.mode === "created" &&
573
595
  target.strategy !== "package-json" &&
@@ -576,6 +598,15 @@ export class RunReconcileUseCase {
576
598
  ? template
577
599
  : this.reconcileContent(target, before, template, input.phasegateVersion);
578
600
  const changed = before !== next;
601
+ // A no-op merge alone cannot attest user-edited content. Recover only a
602
+ // complete canonical output, or a package version-only change whose prior
603
+ // bytes can be reconstructed and matched against the recorded hash.
604
+ const recoveredContent = !changed && !matchesManifest && (
605
+ target.strategy === "package-json"
606
+ ? this.hashCalculator.compute(this.reconcileContent(target, before, template, previousVersion)).equals(entry.hash)
607
+ : before === (entry.mode === "created" && target.strategy !== "markdown-managed" && target.strategy !== "json-named"
608
+ ? template : this.reconcileContent(target, null, template, input.phasegateVersion))
609
+ ) ? before : undefined;
579
610
  const repairMode: RepairMode = matchesManifest ? "mechanical" : "ai-assisted";
580
611
  return {
581
612
  item: this.item(
@@ -584,11 +615,13 @@ export class RunReconcileUseCase {
584
615
  repairMode,
585
616
  target.strategy,
586
617
  changed,
587
- changed ? `${entry.path}: update managed portion` : `${entry.path}: already up to date`,
618
+ changed ? `${entry.path}: update managed portion` : recoveredContent !== undefined
619
+ ? `${entry.path}: content already current; recover interrupted manifest hash` : `${entry.path}: already up to date`,
588
620
  this.diffSummary(before, next),
589
621
  repairMode === "ai-assisted" ? SKILL_HINT : null,
590
622
  ),
591
623
  needsBackup: !matchesManifest || input.force,
624
+ recoveredContent,
592
625
  apply: async () => {
593
626
  await mkdir(dirname(absolutePath), { recursive: true });
594
627
  await writeFile(absolutePath, next, "utf8");
@@ -598,12 +631,20 @@ export class RunReconcileUseCase {
598
631
  };
599
632
  }
600
633
 
634
+ private async contextSkillSet(projectRoot: string, contextPath: string): Promise<SkillSet> {
635
+ const agentRoot = contextPath.includes("CLAUDE") ? ".claude/skills" : ".codex/skills";
636
+ const metadata = await readTextOrNull(join(projectRoot, agentRoot, HARNESS_VERSION_BASENAME))
637
+ ?? await readTextOrNull(join(projectRoot, SHARED_SKILLS_VERSION_PATH));
638
+ return resolveInstalledSkillSet(metadata);
639
+ }
640
+
601
641
  private async planMissingTarget(input: RunReconcileInput, target: ReconcileTarget) {
602
642
  if (target.strategy === "symlink") return this.planSymlink(input.projectRoot, target.path);
603
643
  const absolutePath = this.resolveProjectPath(input.projectRoot, target.path);
604
644
  const before = await readTextOrNull(absolutePath);
605
645
  const rawTemplate = target.templatePath ? await readFile(join(input.harnessRoot, target.templatePath), "utf8") : "";
606
- const template = target.strategy === "markdown-managed" ? renderAgentContextTemplate(rawTemplate) : rawTemplate;
646
+ const template = target.strategy === "markdown-managed"
647
+ ? renderAgentContextTemplate(rawTemplate, await this.contextSkillSet(input.projectRoot, target.path)) : rawTemplate;
607
648
  const next =
608
649
  before === null && target.strategy !== "package-json"
609
650
  ? template
@@ -13,6 +13,7 @@ import {
13
13
  } from "../../application/operator-notice.js";
14
14
  import type { RunInstallUseCase } from "../../application/usecases/run-install.js";
15
15
  import type { AgentTarget } from "../../domain/agent-target.js";
16
+ import type { SkillSet } from "../../application/bundled-skill-selection.js";
16
17
 
17
18
  export interface InstallHandlerInput {
18
19
  readonly projectRoot: string;
@@ -26,7 +27,7 @@ export interface InstallHandlerInput {
26
27
  readonly includeCodex?: boolean;
27
28
  readonly includeHusky?: boolean;
28
29
  readonly includeCi?: boolean;
29
- readonly skillSet?: "core" | "all";
30
+ readonly skillSet?: SkillSet;
30
31
  readonly workflow?: "standard" | "strict";
31
32
  readonly agent?: AgentTarget;
32
33
  readonly personal?: boolean;