specweave 1.0.586 → 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.
- package/bin/specweave.js +30 -0
- package/dist/plugins/specweave/lib/integrations/github/github-access-error.d.ts +48 -0
- package/dist/plugins/specweave/lib/integrations/github/github-access-error.d.ts.map +1 -0
- package/dist/plugins/specweave/lib/integrations/github/github-access-error.js +69 -0
- package/dist/plugins/specweave/lib/integrations/github/github-access-error.js.map +1 -0
- package/dist/plugins/specweave/lib/integrations/github/github-client-v2.d.ts +8 -0
- package/dist/plugins/specweave/lib/integrations/github/github-client-v2.d.ts.map +1 -1
- package/dist/plugins/specweave/lib/integrations/github/github-client-v2.js +22 -2
- package/dist/plugins/specweave/lib/integrations/github/github-client-v2.js.map +1 -1
- package/dist/plugins/specweave/lib/vendor/generators/spec/task-parser.js +38 -16
- package/dist/plugins/specweave/lib/vendor/generators/spec/task-parser.js.map +1 -1
- package/dist/src/cli/commands/handoff.d.ts +54 -0
- package/dist/src/cli/commands/handoff.d.ts.map +1 -0
- package/dist/src/cli/commands/handoff.js +82 -0
- package/dist/src/cli/commands/handoff.js.map +1 -0
- package/dist/src/cli/helpers/init/gitignore-generator.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/gitignore-generator.js +3 -0
- package/dist/src/cli/helpers/init/gitignore-generator.js.map +1 -1
- package/dist/src/core/hooks/handlers/hook-router.d.ts.map +1 -1
- package/dist/src/core/hooks/handlers/hook-router.js +5 -0
- package/dist/src/core/hooks/handlers/hook-router.js.map +1 -1
- package/dist/src/core/hooks/handlers/pre-compact.d.ts +33 -0
- package/dist/src/core/hooks/handlers/pre-compact.d.ts.map +1 -0
- package/dist/src/core/hooks/handlers/pre-compact.js +109 -0
- package/dist/src/core/hooks/handlers/pre-compact.js.map +1 -0
- package/dist/src/core/hooks/handlers/types.d.ts +1 -1
- package/dist/src/core/hooks/handlers/types.d.ts.map +1 -1
- package/dist/src/core/hooks/handlers/types.js +3 -0
- package/dist/src/core/hooks/handlers/types.js.map +1 -1
- package/dist/src/core/session/handoff-doc-format.d.ts +164 -0
- package/dist/src/core/session/handoff-doc-format.d.ts.map +1 -0
- package/dist/src/core/session/handoff-doc-format.js +292 -0
- package/dist/src/core/session/handoff-doc-format.js.map +1 -0
- package/dist/src/core/session/handoff-git-state.d.ts +49 -0
- package/dist/src/core/session/handoff-git-state.d.ts.map +1 -0
- package/dist/src/core/session/handoff-git-state.js +164 -0
- package/dist/src/core/session/handoff-git-state.js.map +1 -0
- package/dist/src/core/session/handoff-secret-scrub.d.ts +59 -0
- package/dist/src/core/session/handoff-secret-scrub.d.ts.map +1 -0
- package/dist/src/core/session/handoff-secret-scrub.js +72 -0
- package/dist/src/core/session/handoff-secret-scrub.js.map +1 -0
- package/dist/src/core/session/{handoff-context.d.ts → install-handoff-context.d.ts} +7 -3
- package/dist/src/core/session/install-handoff-context.d.ts.map +1 -0
- package/dist/src/core/session/{handoff-context.js → install-handoff-context.js} +7 -3
- package/dist/src/core/session/install-handoff-context.js.map +1 -0
- package/dist/src/core/session/work-handoff.d.ts +88 -0
- package/dist/src/core/session/work-handoff.d.ts.map +1 -0
- package/dist/src/core/session/work-handoff.js +412 -0
- package/dist/src/core/session/work-handoff.js.map +1 -0
- package/dist/src/generators/spec/task-parser.d.ts.map +1 -1
- package/dist/src/generators/spec/task-parser.js +38 -16
- package/dist/src/generators/spec/task-parser.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/commands/handoff.md +54 -0
- package/plugins/specweave/lib/integrations/github/github-access-error.js +43 -0
- package/plugins/specweave/lib/integrations/github/github-access-error.ts +103 -0
- package/plugins/specweave/lib/integrations/github/github-client-v2.js +24 -4
- package/plugins/specweave/lib/integrations/github/github-client-v2.ts +26 -4
- package/plugins/specweave/lib/vendor/generators/spec/task-parser.js +38 -16
- package/plugins/specweave/lib/vendor/generators/spec/task-parser.js.map +1 -1
- package/plugins/specweave/skills/handoff/SKILL.md +59 -0
- package/dist/src/core/session/handoff-context.d.ts.map +0 -1
- 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
|