specweave 1.0.585 → 1.0.587

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 (165) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/bin/specweave.js +56 -0
  3. package/dist/plugins/specweave/lib/integrations/github/github-access-error.d.ts +48 -0
  4. package/dist/plugins/specweave/lib/integrations/github/github-access-error.d.ts.map +1 -0
  5. package/dist/plugins/specweave/lib/integrations/github/github-access-error.js +69 -0
  6. package/dist/plugins/specweave/lib/integrations/github/github-access-error.js.map +1 -0
  7. package/dist/plugins/specweave/lib/integrations/github/github-client-v2.d.ts +8 -0
  8. package/dist/plugins/specweave/lib/integrations/github/github-client-v2.d.ts.map +1 -1
  9. package/dist/plugins/specweave/lib/integrations/github/github-client-v2.js +22 -2
  10. package/dist/plugins/specweave/lib/integrations/github/github-client-v2.js.map +1 -1
  11. package/dist/plugins/specweave/lib/vendor/generators/spec/task-parser.js +38 -16
  12. package/dist/plugins/specweave/lib/vendor/generators/spec/task-parser.js.map +1 -1
  13. package/dist/plugins/specweave/lib/vendor/utils/credential-masker.js +11 -1
  14. package/dist/plugins/specweave/lib/vendor/utils/credential-masker.js.map +1 -1
  15. package/dist/src/cli/commands/auto.js +1 -1
  16. package/dist/src/cli/commands/auto.js.map +1 -1
  17. package/dist/src/cli/commands/generate-rubric.d.ts +35 -0
  18. package/dist/src/cli/commands/generate-rubric.d.ts.map +1 -0
  19. package/dist/src/cli/commands/generate-rubric.js +73 -0
  20. package/dist/src/cli/commands/generate-rubric.js.map +1 -0
  21. package/dist/src/cli/commands/get.js +22 -9
  22. package/dist/src/cli/commands/get.js.map +1 -1
  23. package/dist/src/cli/commands/handoff.d.ts +54 -0
  24. package/dist/src/cli/commands/handoff.d.ts.map +1 -0
  25. package/dist/src/cli/commands/handoff.js +82 -0
  26. package/dist/src/cli/commands/handoff.js.map +1 -0
  27. package/dist/src/cli/commands/plan/plan-orchestrator.d.ts.map +1 -1
  28. package/dist/src/cli/commands/plan/plan-orchestrator.js +11 -0
  29. package/dist/src/cli/commands/plan/plan-orchestrator.js.map +1 -1
  30. package/dist/src/cli/commands/sync-health.d.ts.map +1 -1
  31. package/dist/src/cli/commands/sync-health.js +72 -12
  32. package/dist/src/cli/commands/sync-health.js.map +1 -1
  33. package/dist/src/cli/commands/sync-progress.d.ts.map +1 -1
  34. package/dist/src/cli/commands/sync-progress.js +65 -11
  35. package/dist/src/cli/commands/sync-progress.js.map +1 -1
  36. package/dist/src/cli/helpers/get/register-repo.d.ts.map +1 -1
  37. package/dist/src/cli/helpers/get/register-repo.js +28 -2
  38. package/dist/src/cli/helpers/get/register-repo.js.map +1 -1
  39. package/dist/src/cli/helpers/init/gitignore-generator.d.ts.map +1 -1
  40. package/dist/src/cli/helpers/init/gitignore-generator.js +3 -0
  41. package/dist/src/cli/helpers/init/gitignore-generator.js.map +1 -1
  42. package/dist/src/cli/helpers/init/next-steps.js +1 -1
  43. package/dist/src/cli/helpers/init/next-steps.js.map +1 -1
  44. package/dist/src/core/analytics/analytics-collector.d.ts.map +1 -1
  45. package/dist/src/core/analytics/analytics-collector.js +9 -1
  46. package/dist/src/core/analytics/analytics-collector.js.map +1 -1
  47. package/dist/src/core/analytics/event-writer.d.ts.map +1 -1
  48. package/dist/src/core/analytics/event-writer.js +3 -1
  49. package/dist/src/core/analytics/event-writer.js.map +1 -1
  50. package/dist/src/core/config/config-manager.d.ts +5 -0
  51. package/dist/src/core/config/config-manager.d.ts.map +1 -1
  52. package/dist/src/core/config/config-manager.js +58 -1
  53. package/dist/src/core/config/config-manager.js.map +1 -1
  54. package/dist/src/core/credentials/credentials-manager.d.ts +21 -0
  55. package/dist/src/core/credentials/credentials-manager.d.ts.map +1 -1
  56. package/dist/src/core/credentials/credentials-manager.js +38 -0
  57. package/dist/src/core/credentials/credentials-manager.js.map +1 -1
  58. package/dist/src/core/hooks/handlers/hook-router.d.ts.map +1 -1
  59. package/dist/src/core/hooks/handlers/hook-router.js +5 -0
  60. package/dist/src/core/hooks/handlers/hook-router.js.map +1 -1
  61. package/dist/src/core/hooks/handlers/pre-compact.d.ts +33 -0
  62. package/dist/src/core/hooks/handlers/pre-compact.d.ts.map +1 -0
  63. package/dist/src/core/hooks/handlers/pre-compact.js +109 -0
  64. package/dist/src/core/hooks/handlers/pre-compact.js.map +1 -0
  65. package/dist/src/core/hooks/handlers/types.d.ts +1 -1
  66. package/dist/src/core/hooks/handlers/types.d.ts.map +1 -1
  67. package/dist/src/core/hooks/handlers/types.js +3 -0
  68. package/dist/src/core/hooks/handlers/types.js.map +1 -1
  69. package/dist/src/core/increment/completion-validator.d.ts.map +1 -1
  70. package/dist/src/core/increment/completion-validator.js +8 -1
  71. package/dist/src/core/increment/completion-validator.js.map +1 -1
  72. package/dist/src/core/increment/template-creator.d.ts.map +1 -1
  73. package/dist/src/core/increment/template-creator.js +5 -19
  74. package/dist/src/core/increment/template-creator.js.map +1 -1
  75. package/dist/src/core/llm/types.d.ts +5 -5
  76. package/dist/src/core/llm/types.d.ts.map +1 -1
  77. package/dist/src/core/llm/types.js +9 -8
  78. package/dist/src/core/llm/types.js.map +1 -1
  79. package/dist/src/core/rubric/rubric-evaluator.d.ts +25 -1
  80. package/dist/src/core/rubric/rubric-evaluator.d.ts.map +1 -1
  81. package/dist/src/core/rubric/rubric-evaluator.js +108 -1
  82. package/dist/src/core/rubric/rubric-evaluator.js.map +1 -1
  83. package/dist/src/core/rubric/rubric-generator.d.ts +28 -1
  84. package/dist/src/core/rubric/rubric-generator.d.ts.map +1 -1
  85. package/dist/src/core/rubric/rubric-generator.js +46 -6
  86. package/dist/src/core/rubric/rubric-generator.js.map +1 -1
  87. package/dist/src/core/rubric/rubric-parser.d.ts.map +1 -1
  88. package/dist/src/core/rubric/rubric-parser.js +18 -2
  89. package/dist/src/core/rubric/rubric-parser.js.map +1 -1
  90. package/dist/src/core/rubric/types.d.ts +1 -1
  91. package/dist/src/core/rubric/types.d.ts.map +1 -1
  92. package/dist/src/core/rubric/types.js.map +1 -1
  93. package/dist/src/core/session/handoff-doc-format.d.ts +164 -0
  94. package/dist/src/core/session/handoff-doc-format.d.ts.map +1 -0
  95. package/dist/src/core/session/handoff-doc-format.js +292 -0
  96. package/dist/src/core/session/handoff-doc-format.js.map +1 -0
  97. package/dist/src/core/session/handoff-git-state.d.ts +49 -0
  98. package/dist/src/core/session/handoff-git-state.d.ts.map +1 -0
  99. package/dist/src/core/session/handoff-git-state.js +164 -0
  100. package/dist/src/core/session/handoff-git-state.js.map +1 -0
  101. package/dist/src/core/session/handoff-secret-scrub.d.ts +59 -0
  102. package/dist/src/core/session/handoff-secret-scrub.d.ts.map +1 -0
  103. package/dist/src/core/session/handoff-secret-scrub.js +72 -0
  104. package/dist/src/core/session/handoff-secret-scrub.js.map +1 -0
  105. package/dist/src/core/session/{handoff-context.d.ts → install-handoff-context.d.ts} +7 -3
  106. package/dist/src/core/session/install-handoff-context.d.ts.map +1 -0
  107. package/dist/src/core/session/{handoff-context.js → install-handoff-context.js} +7 -3
  108. package/dist/src/core/session/install-handoff-context.js.map +1 -0
  109. package/dist/src/core/session/work-handoff.d.ts +88 -0
  110. package/dist/src/core/session/work-handoff.d.ts.map +1 -0
  111. package/dist/src/core/session/work-handoff.js +412 -0
  112. package/dist/src/core/session/work-handoff.js.map +1 -0
  113. package/dist/src/core/sync/retry-wrapper.d.ts +14 -2
  114. package/dist/src/core/sync/retry-wrapper.d.ts.map +1 -1
  115. package/dist/src/core/sync/retry-wrapper.js +15 -4
  116. package/dist/src/core/sync/retry-wrapper.js.map +1 -1
  117. package/dist/src/generators/spec/task-parser.d.ts.map +1 -1
  118. package/dist/src/generators/spec/task-parser.js +38 -16
  119. package/dist/src/generators/spec/task-parser.js.map +1 -1
  120. package/dist/src/integrations/ado/ado-pat-provider.d.ts +6 -2
  121. package/dist/src/integrations/ado/ado-pat-provider.d.ts.map +1 -1
  122. package/dist/src/integrations/ado/ado-pat-provider.js +16 -22
  123. package/dist/src/integrations/ado/ado-pat-provider.js.map +1 -1
  124. package/dist/src/sync/external-item-sync-service.d.ts.map +1 -1
  125. package/dist/src/sync/external-item-sync-service.js +6 -2
  126. package/dist/src/sync/external-item-sync-service.js.map +1 -1
  127. package/dist/src/sync/resilient-write.d.ts +42 -0
  128. package/dist/src/sync/resilient-write.d.ts.map +1 -0
  129. package/dist/src/sync/resilient-write.js +52 -0
  130. package/dist/src/sync/resilient-write.js.map +1 -0
  131. package/dist/src/sync/story-router.d.ts +10 -2
  132. package/dist/src/sync/story-router.d.ts.map +1 -1
  133. package/dist/src/sync/story-router.js.map +1 -1
  134. package/dist/src/sync/sync-coordinator.d.ts +11 -0
  135. package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
  136. package/dist/src/sync/sync-coordinator.js +69 -26
  137. package/dist/src/sync/sync-coordinator.js.map +1 -1
  138. package/dist/src/sync/sync-target-resolver.d.ts +10 -6
  139. package/dist/src/sync/sync-target-resolver.d.ts.map +1 -1
  140. package/dist/src/sync/sync-target-resolver.js +66 -57
  141. package/dist/src/sync/sync-target-resolver.js.map +1 -1
  142. package/dist/src/utils/credential-masker.d.ts.map +1 -1
  143. package/dist/src/utils/credential-masker.js +11 -1
  144. package/dist/src/utils/credential-masker.js.map +1 -1
  145. package/dist/src/utils/structure-level-detector.d.ts +1 -1
  146. package/dist/src/utils/structure-level-detector.d.ts.map +1 -1
  147. package/dist/src/utils/structure-level-detector.js +23 -4
  148. package/dist/src/utils/structure-level-detector.js.map +1 -1
  149. package/package.json +1 -1
  150. package/plugins/specweave/.claude-plugin/plugin.json +1 -1
  151. package/plugins/specweave/commands/handoff.md +54 -0
  152. package/plugins/specweave/defaults/rubric-defaults.md +6 -2
  153. package/plugins/specweave/lib/integrations/github/github-access-error.js +43 -0
  154. package/plugins/specweave/lib/integrations/github/github-access-error.ts +103 -0
  155. package/plugins/specweave/lib/integrations/github/github-client-v2.js +24 -4
  156. package/plugins/specweave/lib/integrations/github/github-client-v2.ts +26 -4
  157. package/plugins/specweave/lib/vendor/generators/spec/task-parser.js +38 -16
  158. package/plugins/specweave/lib/vendor/generators/spec/task-parser.js.map +1 -1
  159. package/plugins/specweave/lib/vendor/utils/credential-masker.js +11 -1
  160. package/plugins/specweave/lib/vendor/utils/credential-masker.js.map +1 -1
  161. package/plugins/specweave/skills/github-sync/SKILL.md +28 -566
  162. package/plugins/specweave/skills/github-sync/evals/evals.json +3 -3
  163. package/plugins/specweave/skills/handoff/SKILL.md +59 -0
  164. package/dist/src/core/session/handoff-context.d.ts.map +0 -1
  165. package/dist/src/core/session/handoff-context.js.map +0 -1
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Handoff Secret Scrub
3
+ *
4
+ * Regex-based redaction over both the handoff doc's free-text fields AND the
5
+ * captured git diff. This is a HEURISTIC baseline (regex only) — an empty
6
+ * redaction list is NOT a guarantee the content is clean. Opportunistic
7
+ * scanner support (gitleaks/trufflehog) is a deferred enhancement.
8
+ *
9
+ * Each match is replaced with a `[REDACTED-<type>]` marker, and the function
10
+ * returns a per-pattern counts map so the doc's "Redaction" section can report
11
+ * how many token-like strings were masked.
12
+ *
13
+ * Part of increment 0867: Cross-Tool Work Handoff (AC-US6-01, AC-US6-02).
14
+ *
15
+ * @module core/session/handoff-secret-scrub
16
+ */
17
+ /**
18
+ * The 12 redaction patterns (AC-US6-01).
19
+ *
20
+ * Patterns are intentionally conservative: they match the well-known token
21
+ * prefixes plus enough trailing characters to avoid masking the prefix alone.
22
+ * Assignment-style secrets (`password=`, `api_key=`) capture the value up to
23
+ * whitespace.
24
+ */
25
+ export const SECRET_PATTERNS = [
26
+ { type: 'openai-key', regex: /sk-[A-Za-z0-9_-]{16,}/g },
27
+ { type: 'github-token', regex: /ghp_[A-Za-z0-9]{20,}/g },
28
+ { type: 'github-oauth', regex: /gho_[A-Za-z0-9]{20,}/g },
29
+ { type: 'github-server', regex: /ghs_[A-Za-z0-9]{20,}/g },
30
+ { type: 'aws-key', regex: /AKIA[0-9A-Z]{12,}/g },
31
+ { type: 'aws-temp-key', regex: /ASIA[0-9A-Z]{12,}/g },
32
+ { type: 'private-key', regex: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g },
33
+ { type: 'vskill-token', regex: /vsk_[A-Za-z0-9]{16,}/g },
34
+ { type: 'slack-token', regex: /xox[bap]-[A-Za-z0-9-]{10,}/g },
35
+ { type: 'bearer', regex: /Bearer\s+[A-Za-z0-9._~+/=-]{8,}/g },
36
+ { type: 'password', regex: /password=\S+/gi },
37
+ { type: 'api-key', regex: /api_key=\S+/gi },
38
+ ];
39
+ /**
40
+ * Scrub secrets from a block of text.
41
+ *
42
+ * Runs each declared pattern in order. Patterns are applied sequentially over
43
+ * the progressively-scrubbed text, so an already-redacted span cannot be
44
+ * matched a second time by a later pattern.
45
+ *
46
+ * @param text - Free-text or diff content to scrub.
47
+ * @returns Scrubbed text + per-pattern counts (only patterns that fired).
48
+ */
49
+ export function scrubSecrets(text) {
50
+ const counts = {};
51
+ let scrubbed = text ?? '';
52
+ for (const { type, regex } of SECRET_PATTERNS) {
53
+ // Fresh lastIndex per call (regex literals are module-scoped + global).
54
+ regex.lastIndex = 0;
55
+ let matched = 0;
56
+ scrubbed = scrubbed.replace(regex, () => {
57
+ matched += 1;
58
+ return `[REDACTED-${type}]`;
59
+ });
60
+ if (matched > 0) {
61
+ counts[type] = matched;
62
+ }
63
+ }
64
+ return { scrubbed, counts };
65
+ }
66
+ /**
67
+ * Total number of redactions across all patterns.
68
+ */
69
+ export function totalRedactions(counts) {
70
+ return Object.values(counts).reduce((sum, n) => sum + n, 0);
71
+ }
72
+ //# sourceMappingURL=handoff-secret-scrub.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoff-secret-scrub.js","sourceRoot":"","sources":["../../../../src/core/session/handoff-secret-scrub.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAYH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAA6B;IACvD,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,wBAAwB,EAAE;IACvD,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,uBAAuB,EAAE;IACxD,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,uBAAuB,EAAE;IACxD,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,uBAAuB,EAAE;IACzD,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,oBAAoB,EAAE;IAChD,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,oBAAoB,EAAE;IACrD,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,wCAAwC,EAAE;IACxE,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,uBAAuB,EAAE;IACxD,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,6BAA6B,EAAE;IAC7D,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,kCAAkC,EAAE;IAC7D,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,gBAAgB,EAAE;IAC7C,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,eAAe,EAAE;CACnC,CAAC;AAYX;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,IAAI,QAAQ,GAAG,IAAI,IAAI,EAAE,CAAC;IAE1B,KAAK,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,eAAe,EAAE,CAAC;QAC9C,wEAAwE;QACxE,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC;QACpB,IAAI,OAAO,GAAG,CAAC,CAAC;QAChB,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE;YACtC,OAAO,IAAI,CAAC,CAAC;YACb,OAAO,aAAa,IAAI,GAAG,CAAC;QAC9B,CAAC,CAAC,CAAC;QACH,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;YAChB,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC;QACzB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;AAC9B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,MAA8B;IAC5D,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AAC9D,CAAC"}
@@ -1,5 +1,9 @@
1
1
  /**
2
- * Handoff Context Generator
2
+ * Plugin-Install Handoff Context Generator
3
+ *
4
+ * NOTE: This is the legacy plugin-INSTALL handoff (renamed from
5
+ * `handoff-context.ts` in 0867 to free that name for the cross-tool work
6
+ * handoff). It is unrelated to `work-handoff.ts` / `handoff-doc-format.ts`.
3
7
  *
4
8
  * Generates context information for session handoff, enabling users
5
9
  * to continue their work in a new session after plugin installation.
@@ -11,7 +15,7 @@
11
15
  * - Available skills from those plugins
12
16
  * - Suggested continuation prompt
13
17
  *
14
- * @module core/session/handoff-context
18
+ * @module core/session/install-handoff-context
15
19
  */
16
20
  /**
17
21
  * A skill/command available from an installed plugin
@@ -96,4 +100,4 @@ export declare function generateHandoffContext(options: HandoffContextOptions):
96
100
  * ```
97
101
  */
98
102
  export declare function formatHandoffText(options: HandoffContextOptions): string;
99
- //# sourceMappingURL=handoff-context.d.ts.map
103
+ //# sourceMappingURL=install-handoff-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install-handoff-context.d.ts","sourceRoot":"","sources":["../../../../src/core/session/install-handoff-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH;;GAEG;AACH,MAAM,WAAW,KAAK;IACpB,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,gCAAgC;IAChC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,gDAAgD;IAChD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gCAAgC;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,2BAA2B;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gCAAgC;IAChC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,wBAAwB;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,eAAe,EAAE,KAAK,EAAE,CAAC;IACzB,kDAAkD;IAClD,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAgRD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAwBhB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAiExE"}
@@ -1,5 +1,9 @@
1
1
  /**
2
- * Handoff Context Generator
2
+ * Plugin-Install Handoff Context Generator
3
+ *
4
+ * NOTE: This is the legacy plugin-INSTALL handoff (renamed from
5
+ * `handoff-context.ts` in 0867 to free that name for the cross-tool work
6
+ * handoff). It is unrelated to `work-handoff.ts` / `handoff-doc-format.ts`.
3
7
  *
4
8
  * Generates context information for session handoff, enabling users
5
9
  * to continue their work in a new session after plugin installation.
@@ -11,7 +15,7 @@
11
15
  * - Available skills from those plugins
12
16
  * - Suggested continuation prompt
13
17
  *
14
- * @module core/session/handoff-context
18
+ * @module core/session/install-handoff-context
15
19
  */
16
20
  // ============================================================================
17
21
  // Constants - Formatting
@@ -371,4 +375,4 @@ export function formatHandoffText(options) {
371
375
  lines.push(HEAVY_SEPARATOR);
372
376
  return lines.join('\n');
373
377
  }
374
- //# sourceMappingURL=handoff-context.js.map
378
+ //# sourceMappingURL=install-handoff-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install-handoff-context.js","sourceRoot":"","sources":["../../../../src/core/session/install-handoff-context.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Work Handoff Builder
3
+ *
4
+ * Assembles a portable, secret-scrubbed handoff document (+ a full diff of
5
+ * uncommitted edits) from durable on-disk state, so a developer can stop work
6
+ * in one AI tool and resume in another. This is the single deterministic engine
7
+ * behind the `specweave handoff` CLI, the `/sw:handoff` command, and the
8
+ * PreCompact hook — all of which call {@link buildWorkHandoff}.
9
+ *
10
+ * Workspace detection is intentionally NOT a raw `.specweave/` directory test:
11
+ * a stale child-repo `.specweave/` (no real state) would misclassify. We resolve
12
+ * the effective root then require an `active-increment.json` that actually
13
+ * lists increments. Metadata reads are gated with `MetadataManager.exists()`
14
+ * because `MetadataManager.read()` LAZILY CREATES default metadata — a side
15
+ * effect a read-only handoff must never trigger.
16
+ *
17
+ * All increment/task/AC/workspace logic is REUSED from existing modules (DRY):
18
+ * `parseTasksWithUSLinks`, `calculateProgressFromTasksFile`,
19
+ * `ActiveIncrementManager.getActive()`, `MetadataManager`, `resolveEffectiveRoot`.
20
+ *
21
+ * Part of increment 0867: Cross-Tool Work Handoff
22
+ * (AC-US1-03..07, AC-US3-01..05, AC-US4-*, AC-US6-01/02/05).
23
+ *
24
+ * @module core/session/work-handoff
25
+ */
26
+ /**
27
+ * Options controlling a handoff build. All fields are optional; the agent /
28
+ * CLI supplies only the short free-text strings + flags.
29
+ */
30
+ export interface WorkHandoffOptions {
31
+ /** Required disambiguator when 2+ increments are active. */
32
+ incrementId?: string;
33
+ /** "Why I'm handing off" (e.g. "out of tokens"). */
34
+ reason?: string;
35
+ /** Where things stand. */
36
+ summary?: string;
37
+ /** The exact next step. */
38
+ next?: string;
39
+ /** A gotcha / warning for the next agent. */
40
+ gotcha?: string;
41
+ /** Agent-supplied decisions; merged OVER plan.md decisions. */
42
+ decisions?: string[];
43
+ /** Embed the full body in the paste-prompt (cross-machine). */
44
+ inline?: boolean;
45
+ /** Override the doc output path. */
46
+ out?: string;
47
+ /** Force the non-SpecWeave `.handoff/` fallback even in a workspace. */
48
+ nonSpecweave?: boolean;
49
+ }
50
+ /**
51
+ * Result of a handoff build.
52
+ */
53
+ export interface WorkHandoffResult {
54
+ /** Absolute path of the written doc (the CLI prints this FIRST). */
55
+ docPath: string;
56
+ /** Absolute path of the sibling full-diff file. */
57
+ diffPath: string;
58
+ /** The full rendered + scrubbed doc markdown. */
59
+ docMarkdown: string;
60
+ /** The copy-paste resume prompt. */
61
+ pastePrompt: string;
62
+ /** Whether the high-fidelity SpecWeave path was taken. */
63
+ isSpecWeave: boolean;
64
+ }
65
+ /**
66
+ * Thrown when 2+ increments are active and no explicit id was supplied.
67
+ * Carries the candidate ids so the CLI can list them.
68
+ */
69
+ export declare class AmbiguousActiveIncrementError extends Error {
70
+ readonly candidates: string[];
71
+ constructor(candidates: string[]);
72
+ }
73
+ /**
74
+ * Build a handoff for `repoRoot`.
75
+ *
76
+ * @param repoRoot - Where to start workspace resolution from (usually cwd).
77
+ * @param opts - {@link WorkHandoffOptions}.
78
+ * @returns {@link WorkHandoffResult}.
79
+ * @throws {AmbiguousActiveIncrementError} when 2+ active increments + no id.
80
+ */
81
+ export declare function buildWorkHandoff(repoRoot: string, opts?: WorkHandoffOptions): Promise<WorkHandoffResult>;
82
+ /**
83
+ * Ownership sentinel: is a root `./HANDOFF.md` a foreign file (lacks the
84
+ * `Doc format v1` marker)? Exposed for the builder + tests. When foreign, the
85
+ * caller must NOT overwrite it.
86
+ */
87
+ export declare function isForeignHandoffFile(handoffPath: string): boolean;
88
+ //# sourceMappingURL=work-handoff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"work-handoff.d.ts","sourceRoot":"","sources":["../../../../src/core/session/work-handoff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAoBH;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0BAA0B;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2BAA2B;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,oCAAoC;IACpC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,iDAAiD;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,WAAW,EAAE,OAAO,CAAC;CACtB;AAED;;;GAGG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;aAC1B,UAAU,EAAE,MAAM,EAAE;gBAApB,UAAU,EAAE,MAAM,EAAE;CAMjD;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,kBAAuB,GAC5B,OAAO,CAAC,iBAAiB,CAAC,CAmG5B;AAwOD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAIjE"}
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Work Handoff Builder
3
+ *
4
+ * Assembles a portable, secret-scrubbed handoff document (+ a full diff of
5
+ * uncommitted edits) from durable on-disk state, so a developer can stop work
6
+ * in one AI tool and resume in another. This is the single deterministic engine
7
+ * behind the `specweave handoff` CLI, the `/sw:handoff` command, and the
8
+ * PreCompact hook — all of which call {@link buildWorkHandoff}.
9
+ *
10
+ * Workspace detection is intentionally NOT a raw `.specweave/` directory test:
11
+ * a stale child-repo `.specweave/` (no real state) would misclassify. We resolve
12
+ * the effective root then require an `active-increment.json` that actually
13
+ * lists increments. Metadata reads are gated with `MetadataManager.exists()`
14
+ * because `MetadataManager.read()` LAZILY CREATES default metadata — a side
15
+ * effect a read-only handoff must never trigger.
16
+ *
17
+ * All increment/task/AC/workspace logic is REUSED from existing modules (DRY):
18
+ * `parseTasksWithUSLinks`, `calculateProgressFromTasksFile`,
19
+ * `ActiveIncrementManager.getActive()`, `MetadataManager`, `resolveEffectiveRoot`.
20
+ *
21
+ * Part of increment 0867: Cross-Tool Work Handoff
22
+ * (AC-US1-03..07, AC-US3-01..05, AC-US4-*, AC-US6-01/02/05).
23
+ *
24
+ * @module core/session/work-handoff
25
+ */
26
+ import * as fs from 'fs';
27
+ import * as path from 'path';
28
+ import { resolveEffectiveRoot } from '../../utils/find-project-root.js';
29
+ import { ActiveIncrementManager } from '../increment/active-increment-manager.js';
30
+ import { MetadataManager } from '../increment/metadata-manager.js';
31
+ import { parseTasksWithUSLinks } from '../../generators/spec/task-parser.js';
32
+ import { calculateProgressFromTasksFile } from '../../progress/us-progress-tracker.js';
33
+ import { captureGitState } from './handoff-git-state.js';
34
+ import { scrubSecrets } from './handoff-secret-scrub.js';
35
+ import { renderHandoffDoc, renderPastePrompt, DOC_FORMAT_MARKER, } from './handoff-doc-format.js';
36
+ /**
37
+ * Thrown when 2+ increments are active and no explicit id was supplied.
38
+ * Carries the candidate ids so the CLI can list them.
39
+ */
40
+ export class AmbiguousActiveIncrementError extends Error {
41
+ constructor(candidates) {
42
+ super(`Multiple active increments — pass an explicit id. Candidates: ${candidates.join(', ')}`);
43
+ this.candidates = candidates;
44
+ this.name = 'AmbiguousActiveIncrementError';
45
+ }
46
+ }
47
+ /**
48
+ * Build a handoff for `repoRoot`.
49
+ *
50
+ * @param repoRoot - Where to start workspace resolution from (usually cwd).
51
+ * @param opts - {@link WorkHandoffOptions}.
52
+ * @returns {@link WorkHandoffResult}.
53
+ * @throws {AmbiguousActiveIncrementError} when 2+ active increments + no id.
54
+ */
55
+ export async function buildWorkHandoff(repoRoot, opts = {}) {
56
+ // Resolve the effective workspace root. `resolveEffectiveRoot` returns
57
+ // process.cwd() as a last resort when the start dir is not inside any
58
+ // SpecWeave/umbrella tree — which would wrongly anchor a plain repo to the
59
+ // caller's cwd. So we only ACCEPT the resolved root when it actually carries
60
+ // SpecWeave state; otherwise we anchor to the passed `repoRoot` itself.
61
+ const resolved = resolveEffectiveRoot(repoRoot);
62
+ const resolvedHasState = fs.existsSync(path.join(resolved, '.specweave', 'state', 'active-increment.json'));
63
+ const passedRoot = path.resolve(repoRoot);
64
+ const effectiveRoot = !opts.nonSpecweave && resolvedHasState ? resolved : passedRoot;
65
+ // ── Workspace classification ──────────────────────────────────────────────
66
+ // SpecWeave only if (a) not forced off and (b) a real active-increment.json
67
+ // exists at the effective root. A stale .specweave/ with empty/missing
68
+ // active-increment.json classifies as non-SpecWeave.
69
+ const hasSpecweaveState = opts.nonSpecweave
70
+ ? false
71
+ : fs.existsSync(path.join(effectiveRoot, '.specweave', 'state', 'active-increment.json'));
72
+ const activeIds = hasSpecweaveState ? readActiveIds(effectiveRoot) : [];
73
+ // ── Resolve which increment (if any) ──────────────────────────────────────
74
+ let incrementId;
75
+ if (hasSpecweaveState && activeIds.length > 0) {
76
+ if (activeIds.length === 1) {
77
+ incrementId = activeIds[0];
78
+ }
79
+ else {
80
+ // 2+ active.
81
+ if (!opts.incrementId)
82
+ throw new AmbiguousActiveIncrementError(activeIds);
83
+ incrementId = opts.incrementId;
84
+ }
85
+ }
86
+ // A workspace is "SpecWeave" for doc purposes when it has SpecWeave state.
87
+ // (0 active still uses the SpecWeave write paths but with a git+config doc.)
88
+ const isSpecWeave = hasSpecweaveState && !opts.nonSpecweave;
89
+ // ── Assemble increment facts (gated, no side effects) ─────────────────────
90
+ let increment;
91
+ let planDecisions = [];
92
+ if (incrementId && MetadataManager.exists(incrementId, effectiveRoot)) {
93
+ const incDir = path.join(effectiveRoot, '.specweave', 'increments', incrementId);
94
+ increment = await assembleIncrementInfo(incDir, incrementId, effectiveRoot);
95
+ planDecisions = readPlanDecisions(path.join(incDir, 'plan.md'));
96
+ }
97
+ // ── Ambient rules from config.json ────────────────────────────────────────
98
+ const ambient = isSpecWeave ? readAmbientRules(effectiveRoot) : undefined;
99
+ // ── Decide write paths (ownership sentinel for non-SpecWeave) ─────────────
100
+ const { docPath, diffPath } = resolveWritePaths(effectiveRoot, isSpecWeave, incrementId, opts.out);
101
+ // ── Git state + full diff dump (free, no tokens) ──────────────────────────
102
+ const git = captureGitState(effectiveRoot, diffPath);
103
+ // ── Scrub free-text + the captured diff before any write ──────────────────
104
+ const mergedDecisions = [...planDecisions, ...(opts.decisions ?? [])];
105
+ const scrubbedText = scrubFields({
106
+ reason: opts.reason,
107
+ summary: opts.summary,
108
+ next: opts.next,
109
+ gotcha: opts.gotcha,
110
+ decisions: mergedDecisions,
111
+ });
112
+ const redactionCounts = scrubbedText.counts;
113
+ // Re-scrub the diff file in place (captureGitState wrote the raw diff).
114
+ scrubDiffFileInPlace(diffPath, redactionCounts);
115
+ // ── Render ────────────────────────────────────────────────────────────────
116
+ const docInput = {
117
+ docPath,
118
+ diffPath,
119
+ repoRoot: effectiveRoot,
120
+ generatedAt: new Date().toISOString(),
121
+ isSpecWeave,
122
+ reason: scrubbedText.reason,
123
+ summary: scrubbedText.summary,
124
+ next: scrubbedText.next,
125
+ gotcha: scrubbedText.gotcha,
126
+ decisions: scrubbedText.decisions,
127
+ increment,
128
+ ambient,
129
+ git,
130
+ redactionCounts,
131
+ };
132
+ const docMarkdown = renderHandoffDoc(docInput);
133
+ const pastePrompt = renderPastePrompt(docInput, { inline: opts.inline });
134
+ // ── Write doc(s) ──────────────────────────────────────────────────────────
135
+ writeDoc(docPath, docMarkdown);
136
+ // SpecWeave + active increment: also write the stable convenience copy.
137
+ if (isSpecWeave && incrementId && !opts.out) {
138
+ const latest = path.join(effectiveRoot, '.specweave', 'state', 'handoff-latest.md');
139
+ writeDoc(latest, docMarkdown);
140
+ }
141
+ return { docPath, diffPath, docMarkdown, pastePrompt, isSpecWeave };
142
+ }
143
+ // ───────────────────────────────────────────────────────────────────────────
144
+ // Internals
145
+ // ───────────────────────────────────────────────────────────────────────────
146
+ /** Read active increment ids straight from the state file (no lazy create). */
147
+ function readActiveIds(effectiveRoot) {
148
+ try {
149
+ const mgr = new ActiveIncrementManager(effectiveRoot);
150
+ return mgr.getActive();
151
+ }
152
+ catch {
153
+ return [];
154
+ }
155
+ }
156
+ /**
157
+ * Assemble per-increment facts: status, current/next task, task% and AC counts,
158
+ * acSyncEvents drift. Reuses the shared parsers; never lazily creates metadata.
159
+ */
160
+ async function assembleIncrementInfo(incDir, incrementId, effectiveRoot) {
161
+ // Status comes from the already-existing metadata file (exists()-gated by caller).
162
+ let status = 'unknown';
163
+ let acSyncEvents = [];
164
+ try {
165
+ const metaRaw = JSON.parse(fs.readFileSync(path.join(incDir, 'metadata.json'), 'utf-8'));
166
+ if (metaRaw.status)
167
+ status = metaRaw.status;
168
+ // acSyncEvents is stored dynamically (not in the typed interface) — read defensively.
169
+ if (Array.isArray(metaRaw.acSyncEvents)) {
170
+ acSyncEvents = metaRaw.acSyncEvents.slice(0, 5).map((ev) => {
171
+ const updated = ev.updated?.length ?? ev.changesCount ?? 0;
172
+ const conflicts = ev.conflicts?.length ?? 0;
173
+ return `${ev.timestamp}: ${updated} ACs updated, ${conflicts} conflicts`;
174
+ });
175
+ }
176
+ }
177
+ catch {
178
+ // Metadata unreadable — leave defaults.
179
+ }
180
+ const title = readSpecTitle(path.join(incDir, 'spec.md'));
181
+ // Tasks: counts/% via the shared progress fn; current = first non-completed,
182
+ // next = the one after it (from the same parser's status — fixed in 0867 to
183
+ // read the canonical one-line `… | **Status**: [x] …` format correctly).
184
+ const tasksPath = path.join(incDir, 'tasks.md');
185
+ let currentTask;
186
+ let nextTask;
187
+ let doneTasks = 0;
188
+ let totalTasks = 0;
189
+ let taskPercentage = 0;
190
+ if (fs.existsSync(tasksPath)) {
191
+ const progress = await calculateProgressFromTasksFile(tasksPath);
192
+ doneTasks = progress.completedTasks;
193
+ totalTasks = progress.totalTasks;
194
+ taskPercentage = progress.percentage;
195
+ const allTasks = Object.values(parseTasksWithUSLinks(tasksPath)).flat();
196
+ const pending = allTasks
197
+ .filter((t) => t.status !== 'completed' && t.status !== 'canceled')
198
+ // parseTasksWithUSLinks groups by user story, so the flattened order is
199
+ // US-major, not T-id order. Sort by numeric T-id so "current"/"next"
200
+ // reflect real task sequence even with interleaved per-US numbering.
201
+ .sort((a, b) => taskIdNum(a.id) - taskIdNum(b.id));
202
+ if (pending.length > 0)
203
+ currentTask = `${pending[0].id}: ${pending[0].title}`;
204
+ if (pending.length > 1)
205
+ nextTask = `${pending[1].id}: ${pending[1].title}`;
206
+ }
207
+ // ACs from spec.md checkboxes.
208
+ const { doneAcs, totalAcs } = countSpecAcs(path.join(incDir, 'spec.md'));
209
+ return {
210
+ id: incrementId,
211
+ status,
212
+ title,
213
+ currentTask,
214
+ nextTask,
215
+ doneTasks,
216
+ totalTasks,
217
+ taskPercentage,
218
+ doneAcs,
219
+ totalAcs,
220
+ acSyncEvents,
221
+ };
222
+ }
223
+ /** Numeric portion of a `T-007` / `T-012E` task id, for ordering. */
224
+ function taskIdNum(id) {
225
+ const m = id.match(/T-(\d+)/);
226
+ return m ? parseInt(m[1], 10) : Number.MAX_SAFE_INTEGER;
227
+ }
228
+ /** Count `- [ ] AC-...` / `- [x] AC-...` checkboxes in spec.md. */
229
+ function countSpecAcs(specPath) {
230
+ if (!fs.existsSync(specPath))
231
+ return { doneAcs: 0, totalAcs: 0 };
232
+ const acRegex = /^-\s+\[([ x])\]\s+\*{0,2}(AC-[A-Z0-9-]+)\*{0,2}/;
233
+ let done = 0;
234
+ let total = 0;
235
+ for (const line of fs.readFileSync(specPath, 'utf-8').split('\n')) {
236
+ const m = line.match(acRegex);
237
+ if (m) {
238
+ total += 1;
239
+ if (m[1] === 'x')
240
+ done += 1;
241
+ }
242
+ }
243
+ return { doneAcs: done, totalAcs: total };
244
+ }
245
+ /** Read the `title:` from spec.md frontmatter, if present. */
246
+ function readSpecTitle(specPath) {
247
+ if (!fs.existsSync(specPath))
248
+ return undefined;
249
+ const content = fs.readFileSync(specPath, 'utf-8');
250
+ const m = content.match(/^title:\s*["']?(.+?)["']?\s*$/m);
251
+ return m ? m[1] : undefined;
252
+ }
253
+ /**
254
+ * Extract decision-ish bullets from plan.md `## Approach`, `## Components`,
255
+ * `## Risks` sections. Best-effort: each bullet line becomes a decision.
256
+ */
257
+ function readPlanDecisions(planPath) {
258
+ if (!fs.existsSync(planPath))
259
+ return [];
260
+ const lines = fs.readFileSync(planPath, 'utf-8').split('\n');
261
+ const wanted = new Set(['approach', 'risks', 'key decisions', 'decisions']);
262
+ const decisions = [];
263
+ let capture = false;
264
+ for (const line of lines) {
265
+ const heading = line.match(/^##\s+(.+?)\s*$/);
266
+ if (heading) {
267
+ capture = wanted.has(heading[1].trim().toLowerCase());
268
+ continue;
269
+ }
270
+ if (capture) {
271
+ const bullet = line.match(/^[-*]\s+(.+)/);
272
+ if (bullet)
273
+ decisions.push(bullet[1].trim());
274
+ }
275
+ }
276
+ return decisions.slice(0, 10);
277
+ }
278
+ /** Read ambient rules (test mode / coverage target / WIP limit) from config.json. */
279
+ function readAmbientRules(effectiveRoot) {
280
+ try {
281
+ const cfg = JSON.parse(fs.readFileSync(path.join(effectiveRoot, '.specweave', 'config.json'), 'utf-8'));
282
+ return {
283
+ testMode: cfg.testing?.defaultTestMode,
284
+ coverageTarget: cfg.testing?.defaultCoverageTarget,
285
+ wipLimit: cfg.limits?.maxActiveIncrements,
286
+ };
287
+ }
288
+ catch {
289
+ return {};
290
+ }
291
+ }
292
+ /**
293
+ * Decide doc + diff paths.
294
+ *
295
+ * - explicit `out`: use it (diff is a sibling `.diff`).
296
+ * - SpecWeave + active increment: `reports/handoff.md` (the stable copy is
297
+ * written separately by the caller).
298
+ * - SpecWeave, no active increment: `state/handoff-latest.md`.
299
+ * - non-SpecWeave: `.handoff/HANDOFF.md`, unless a foreign root `./HANDOFF.md`
300
+ * without the marker exists (ownership sentinel still routes to `.handoff/`).
301
+ */
302
+ function resolveWritePaths(effectiveRoot, isSpecWeave, incrementId, out) {
303
+ if (out) {
304
+ const abs = path.isAbsolute(out) ? out : path.join(effectiveRoot, out);
305
+ return { docPath: abs, diffPath: siblingDiff(abs) };
306
+ }
307
+ if (isSpecWeave && incrementId) {
308
+ const docPath = path.join(effectiveRoot, '.specweave', 'increments', incrementId, 'reports', 'handoff.md');
309
+ const diffPath = path.join(effectiveRoot, '.specweave', 'state', 'handoff-latest.diff');
310
+ return { docPath, diffPath };
311
+ }
312
+ if (isSpecWeave) {
313
+ const docPath = path.join(effectiveRoot, '.specweave', 'state', 'handoff-latest.md');
314
+ const diffPath = path.join(effectiveRoot, '.specweave', 'state', 'handoff-latest.diff');
315
+ return { docPath, diffPath };
316
+ }
317
+ // Non-SpecWeave. Default target is the repo-root ./HANDOFF.md, but only if it
318
+ // is OURS: a root ./HANDOFF.md that already carries the Doc format v1 marker
319
+ // is a prior handoff we may overwrite in-place. A root ./HANDOFF.md WITHOUT
320
+ // the marker is a foreign file (a project's own HANDOFF) — the ownership
321
+ // sentinel refuses to clobber it and routes to .handoff/ instead.
322
+ const rootHandoff = path.join(effectiveRoot, 'HANDOFF.md');
323
+ if (fs.existsSync(rootHandoff) && !isForeignHandoffFile(rootHandoff)) {
324
+ return { docPath: rootHandoff, diffPath: siblingDiff(rootHandoff) };
325
+ }
326
+ // No (safe) root file → write under .handoff/ (also the foreign-file case).
327
+ ensureHandoffDir(effectiveRoot);
328
+ const docPath = path.join(effectiveRoot, '.handoff', 'HANDOFF.md');
329
+ const diffPath = path.join(effectiveRoot, '.handoff', 'handoff.diff');
330
+ return { docPath, diffPath };
331
+ }
332
+ /** Sibling `<name>.diff` for an arbitrary doc path. */
333
+ function siblingDiff(docPath) {
334
+ const ext = path.extname(docPath);
335
+ return docPath.slice(0, docPath.length - ext.length) + '.diff';
336
+ }
337
+ /**
338
+ * Create `.handoff/` + a `.gitignore` containing `*` so the doc, diff, and any
339
+ * scrubbed-but-still-sensitive content never enter git by default.
340
+ */
341
+ function ensureHandoffDir(effectiveRoot) {
342
+ const dir = path.join(effectiveRoot, '.handoff');
343
+ fs.mkdirSync(dir, { recursive: true });
344
+ const gi = path.join(dir, '.gitignore');
345
+ if (!fs.existsSync(gi))
346
+ fs.writeFileSync(gi, '*\n', 'utf-8');
347
+ }
348
+ /**
349
+ * Ownership sentinel: is a root `./HANDOFF.md` a foreign file (lacks the
350
+ * `Doc format v1` marker)? Exposed for the builder + tests. When foreign, the
351
+ * caller must NOT overwrite it.
352
+ */
353
+ export function isForeignHandoffFile(handoffPath) {
354
+ if (!fs.existsSync(handoffPath))
355
+ return false;
356
+ const content = fs.readFileSync(handoffPath, 'utf-8');
357
+ return !content.includes(DOC_FORMAT_MARKER);
358
+ }
359
+ /** Write a doc, creating parent dirs. */
360
+ function writeDoc(docPath, markdown) {
361
+ fs.mkdirSync(path.dirname(docPath), { recursive: true });
362
+ fs.writeFileSync(docPath, markdown, 'utf-8');
363
+ }
364
+ /** Scrub the free-text fields, accumulating per-pattern counts. */
365
+ function scrubFields(fields) {
366
+ const counts = {};
367
+ const add = (c) => {
368
+ for (const [k, v] of Object.entries(c))
369
+ counts[k] = (counts[k] ?? 0) + v;
370
+ };
371
+ const one = (s) => {
372
+ if (s == null)
373
+ return s;
374
+ const r = scrubSecrets(s);
375
+ add(r.counts);
376
+ return r.scrubbed;
377
+ };
378
+ const decisions = fields.decisions.map((d) => {
379
+ const r = scrubSecrets(d);
380
+ add(r.counts);
381
+ return r.scrubbed;
382
+ });
383
+ return {
384
+ reason: one(fields.reason),
385
+ summary: one(fields.summary),
386
+ next: one(fields.next),
387
+ gotcha: one(fields.gotcha),
388
+ decisions,
389
+ counts,
390
+ };
391
+ }
392
+ /**
393
+ * Re-read the raw diff file captureGitState wrote, scrub it, write it back, and
394
+ * fold its redaction counts into the running totals.
395
+ */
396
+ function scrubDiffFileInPlace(diffPath, counts) {
397
+ try {
398
+ if (!fs.existsSync(diffPath))
399
+ return;
400
+ const raw = fs.readFileSync(diffPath, 'utf-8');
401
+ if (!raw)
402
+ return;
403
+ const { scrubbed, counts: diffCounts } = scrubSecrets(raw);
404
+ fs.writeFileSync(diffPath, scrubbed, 'utf-8');
405
+ for (const [k, v] of Object.entries(diffCounts))
406
+ counts[k] = (counts[k] ?? 0) + v;
407
+ }
408
+ catch {
409
+ // Best-effort.
410
+ }
411
+ }
412
+ //# sourceMappingURL=work-handoff.js.map