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.
Files changed (63) hide show
  1. package/bin/specweave.js +30 -0
  2. package/dist/plugins/specweave/lib/integrations/github/github-access-error.d.ts +48 -0
  3. package/dist/plugins/specweave/lib/integrations/github/github-access-error.d.ts.map +1 -0
  4. package/dist/plugins/specweave/lib/integrations/github/github-access-error.js +69 -0
  5. package/dist/plugins/specweave/lib/integrations/github/github-access-error.js.map +1 -0
  6. package/dist/plugins/specweave/lib/integrations/github/github-client-v2.d.ts +8 -0
  7. package/dist/plugins/specweave/lib/integrations/github/github-client-v2.d.ts.map +1 -1
  8. package/dist/plugins/specweave/lib/integrations/github/github-client-v2.js +22 -2
  9. package/dist/plugins/specweave/lib/integrations/github/github-client-v2.js.map +1 -1
  10. package/dist/plugins/specweave/lib/vendor/generators/spec/task-parser.js +38 -16
  11. package/dist/plugins/specweave/lib/vendor/generators/spec/task-parser.js.map +1 -1
  12. package/dist/src/cli/commands/handoff.d.ts +54 -0
  13. package/dist/src/cli/commands/handoff.d.ts.map +1 -0
  14. package/dist/src/cli/commands/handoff.js +82 -0
  15. package/dist/src/cli/commands/handoff.js.map +1 -0
  16. package/dist/src/cli/helpers/init/gitignore-generator.d.ts.map +1 -1
  17. package/dist/src/cli/helpers/init/gitignore-generator.js +3 -0
  18. package/dist/src/cli/helpers/init/gitignore-generator.js.map +1 -1
  19. package/dist/src/core/hooks/handlers/hook-router.d.ts.map +1 -1
  20. package/dist/src/core/hooks/handlers/hook-router.js +5 -0
  21. package/dist/src/core/hooks/handlers/hook-router.js.map +1 -1
  22. package/dist/src/core/hooks/handlers/pre-compact.d.ts +33 -0
  23. package/dist/src/core/hooks/handlers/pre-compact.d.ts.map +1 -0
  24. package/dist/src/core/hooks/handlers/pre-compact.js +109 -0
  25. package/dist/src/core/hooks/handlers/pre-compact.js.map +1 -0
  26. package/dist/src/core/hooks/handlers/types.d.ts +1 -1
  27. package/dist/src/core/hooks/handlers/types.d.ts.map +1 -1
  28. package/dist/src/core/hooks/handlers/types.js +3 -0
  29. package/dist/src/core/hooks/handlers/types.js.map +1 -1
  30. package/dist/src/core/session/handoff-doc-format.d.ts +164 -0
  31. package/dist/src/core/session/handoff-doc-format.d.ts.map +1 -0
  32. package/dist/src/core/session/handoff-doc-format.js +292 -0
  33. package/dist/src/core/session/handoff-doc-format.js.map +1 -0
  34. package/dist/src/core/session/handoff-git-state.d.ts +49 -0
  35. package/dist/src/core/session/handoff-git-state.d.ts.map +1 -0
  36. package/dist/src/core/session/handoff-git-state.js +164 -0
  37. package/dist/src/core/session/handoff-git-state.js.map +1 -0
  38. package/dist/src/core/session/handoff-secret-scrub.d.ts +59 -0
  39. package/dist/src/core/session/handoff-secret-scrub.d.ts.map +1 -0
  40. package/dist/src/core/session/handoff-secret-scrub.js +72 -0
  41. package/dist/src/core/session/handoff-secret-scrub.js.map +1 -0
  42. package/dist/src/core/session/{handoff-context.d.ts → install-handoff-context.d.ts} +7 -3
  43. package/dist/src/core/session/install-handoff-context.d.ts.map +1 -0
  44. package/dist/src/core/session/{handoff-context.js → install-handoff-context.js} +7 -3
  45. package/dist/src/core/session/install-handoff-context.js.map +1 -0
  46. package/dist/src/core/session/work-handoff.d.ts +88 -0
  47. package/dist/src/core/session/work-handoff.d.ts.map +1 -0
  48. package/dist/src/core/session/work-handoff.js +412 -0
  49. package/dist/src/core/session/work-handoff.js.map +1 -0
  50. package/dist/src/generators/spec/task-parser.d.ts.map +1 -1
  51. package/dist/src/generators/spec/task-parser.js +38 -16
  52. package/dist/src/generators/spec/task-parser.js.map +1 -1
  53. package/package.json +1 -1
  54. package/plugins/specweave/commands/handoff.md +54 -0
  55. package/plugins/specweave/lib/integrations/github/github-access-error.js +43 -0
  56. package/plugins/specweave/lib/integrations/github/github-access-error.ts +103 -0
  57. package/plugins/specweave/lib/integrations/github/github-client-v2.js +24 -4
  58. package/plugins/specweave/lib/integrations/github/github-client-v2.ts +26 -4
  59. package/plugins/specweave/lib/vendor/generators/spec/task-parser.js +38 -16
  60. package/plugins/specweave/lib/vendor/generators/spec/task-parser.js.map +1 -1
  61. package/plugins/specweave/skills/handoff/SKILL.md +59 -0
  62. package/dist/src/core/session/handoff-context.d.ts.map +0 -1
  63. package/dist/src/core/session/handoff-context.js.map +0 -1
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Handoff Doc Format — single source of truth
3
+ *
4
+ * Renders BOTH the durable handoff document and the copy-paste resume prompt
5
+ * from one {@link HandoffDocInput}. The CLI command, the PreCompact hook
6
+ * handler, and the vitest parity test all render through this module, so the
7
+ * doc format and paste-prompt cannot drift between code paths. The vskill
8
+ * self-contained skill inlines a byte-compatible template that references the
9
+ * same canonical section order + footer marker exported here.
10
+ *
11
+ * Design rules:
12
+ * - This module is PURE rendering. It does no IO, no git, no workspace
13
+ * detection — callers pass already-captured + already-scrubbed data in.
14
+ * - The absolute doc path, the `.diff` path, and the per-tool resume matrix are
15
+ * inputs/constants so the renderer is deterministic and testable.
16
+ * - The `Doc format v1` footer marker (DOC_FORMAT_MARKER) is the ownership
17
+ * sentinel that lets the builder know a file is a prior handoff it may
18
+ * overwrite, and the version handle for future format migrations.
19
+ *
20
+ * Part of increment 0867: Cross-Tool Work Handoff
21
+ * (AC-US1-02, AC-US2-04, AC-US5-01..04, AC-US6-02, AC-US6-05).
22
+ *
23
+ * @module core/session/handoff-doc-format
24
+ */
25
+ /**
26
+ * Footer marker stamped at the bottom of every handoff doc.
27
+ *
28
+ * Doubles as the ownership sentinel: a root `./HANDOFF.md` containing this
29
+ * string is treated as a prior handoff (safe to overwrite); one lacking it is a
30
+ * foreign file the builder must not clobber.
31
+ */
32
+ export const DOC_FORMAT_MARKER = 'Doc format v1';
33
+ /**
34
+ * Delimiters wrapping the self-contained body in `--inline` paste-prompts.
35
+ * A resuming agent reads everything between these markers as the handoff.
36
+ */
37
+ export const INLINE_BEGIN_MARKER = 'BEGIN HANDOFF';
38
+ export const INLINE_END_MARKER = 'END HANDOFF';
39
+ /**
40
+ * Canonical section headers, in render order. Exported so the vskill inlined
41
+ * template and the format-parity test (T-017) reference ONE source and fail at
42
+ * build time if either side reorders or renames a section.
43
+ */
44
+ export const HANDOFF_SECTION_ORDER = [
45
+ 'Where I Left Off',
46
+ 'Done / Pending',
47
+ 'Key Decisions & Gotchas',
48
+ 'Files Touched',
49
+ 'Exact Next Steps',
50
+ 'How To Resume',
51
+ 'Redaction',
52
+ ];
53
+ /**
54
+ * The cross-tool resume matrix (AC-US5-01, AC-US5-02).
55
+ *
56
+ * NOTE on Claude: session files live under
57
+ * `~/.claude/projects/<munged-cwd>/<uuid>.jsonl`, where the cwd is munged by
58
+ * replacing EVERY non-alphanumeric char with `-` and NOT collapsing runs — so a
59
+ * leading slash and adjacent separators each become their own dash. Example:
60
+ * /Users/.../specweave-umb/.claude-worktrees/x
61
+ * → -Users-...-specweave-umb--claude-worktrees-x
62
+ * (the `/.` between `umb` and `claude-worktrees` yields a double dash).
63
+ */
64
+ export const CLAUDE_MUNGE_EXAMPLE = '/Users/antonabyzov/Projects/github/specweave-umb/.claude-worktrees/x ' +
65
+ '→ -Users-antonabyzov-Projects-github-specweave-umb--claude-worktrees-x';
66
+ export const TOOL_RESUME_MATRIX = [
67
+ {
68
+ tool: 'Claude Code',
69
+ findSession: 'ls ~/.claude/projects/<munged-cwd>/ (munge: every non-alphanumeric char → "-", runs NOT collapsed; e.g. ' +
70
+ CLAUDE_MUNGE_EXAMPLE + ')',
71
+ resumeCmd: 'claude -r <uuid>',
72
+ },
73
+ {
74
+ tool: 'Codex',
75
+ findSession: 'ls ~/.codex/sessions/ (newest dir = most recent session)',
76
+ resumeCmd: 'codex resume <uuid> (or: codex resume --last)',
77
+ },
78
+ {
79
+ tool: 'OpenCode',
80
+ findSession: 'opencode sessions list',
81
+ resumeCmd: 'opencode -s <id> (long form: opencode --session <id>)',
82
+ },
83
+ {
84
+ tool: 'Gemini CLI',
85
+ findSession: 'run /chat list inside the Gemini session to see saved tags',
86
+ resumeCmd: '/chat resume <tag>',
87
+ },
88
+ {
89
+ tool: 'Antigravity',
90
+ findSession: 'open the Antigravity Agent Manager and pick the prior task thread',
91
+ resumeCmd: 'resume the thread from the Antigravity Agent Manager',
92
+ },
93
+ {
94
+ tool: 'Aider',
95
+ findSession: 'aider keeps .aider.chat.history.md in the repo root',
96
+ resumeCmd: 'aider --restore-chat-history',
97
+ },
98
+ ];
99
+ function fmtCount(counts) {
100
+ const entries = Object.entries(counts);
101
+ if (entries.length === 0) {
102
+ return '_No token-like strings were detected._';
103
+ }
104
+ return entries
105
+ .map(([type, n]) => `- ${n} \`${type}\` ${n === 1 ? 'string' : 'strings'} masked`)
106
+ .join('\n');
107
+ }
108
+ /**
109
+ * Render the per-tool "find your source session" block.
110
+ */
111
+ export function renderResumeMatrix() {
112
+ const lines = ['## How To Resume', ''];
113
+ lines.push('If the doc path above does NOT exist on the machine you are reading this on, ' +
114
+ 'STOP and ask the user to paste the handoff — do not improvise context.');
115
+ lines.push('');
116
+ lines.push('To recover the ORIGINAL transcript (optional), find your source session per tool:');
117
+ lines.push('');
118
+ for (const e of TOOL_RESUME_MATRIX) {
119
+ lines.push(`### ${e.tool}`);
120
+ lines.push(`- Find session: ${e.findSession}`);
121
+ lines.push(`- Resume: \`${e.resumeCmd}\``);
122
+ lines.push('');
123
+ }
124
+ return lines.join('\n').trimEnd();
125
+ }
126
+ /**
127
+ * Render the full handoff document body (markdown).
128
+ *
129
+ * Section order is locked to {@link HANDOFF_SECTION_ORDER}; the `Doc format v1`
130
+ * footer marker is always last.
131
+ */
132
+ export function renderHandoffDoc(input) {
133
+ const L = [];
134
+ // ── Header ──────────────────────────────────────────────────────────────
135
+ const titleBit = input.increment?.title ? `: ${input.increment.title}` : '';
136
+ L.push(`# Work Handoff${titleBit}`);
137
+ L.push('');
138
+ L.push(`- Doc path: ${input.docPath}`);
139
+ L.push(`- Doc link: [${input.docPath}](${input.docPath})`);
140
+ L.push(`- Diff file: ${input.diffPath}`);
141
+ L.push(`- Generated: ${input.generatedAt}`);
142
+ L.push(`- Workspace: ${input.repoRoot} ${input.isSpecWeave ? '(SpecWeave)' : '(non-SpecWeave)'}`);
143
+ if (input.git.isGitRepo) {
144
+ L.push(`- Git: branch \`${input.git.branch || '(detached)'}\` @ \`${input.git.shortSha || '(no commits)'}\``);
145
+ }
146
+ L.push('');
147
+ // ── Where I Left Off ──────────────────────────────────────────────────────
148
+ L.push('## Where I Left Off');
149
+ L.push('');
150
+ if (input.reason)
151
+ L.push(`**Why handing off:** ${input.reason}`);
152
+ if (input.summary)
153
+ L.push(`**Summary:** ${input.summary}`);
154
+ if (input.increment) {
155
+ L.push(`**Increment:** ${input.increment.id} (status: ${input.increment.status})`);
156
+ if (input.increment.currentTask)
157
+ L.push(`**Current task:** ${input.increment.currentTask}`);
158
+ if (input.increment.nextTask)
159
+ L.push(`**Next task:** ${input.increment.nextTask}`);
160
+ }
161
+ else {
162
+ L.push('_No active SpecWeave increment — this is a git + interview handoff._');
163
+ }
164
+ L.push('');
165
+ // ── Done / Pending ──────────────────────────────────────────────────────
166
+ L.push('## Done / Pending');
167
+ L.push('');
168
+ if (input.increment) {
169
+ const inc = input.increment;
170
+ L.push(`- Tasks: ${inc.doneTasks}/${inc.totalTasks} done (${inc.taskPercentage}%), ` +
171
+ `${inc.totalTasks - inc.doneTasks} pending`);
172
+ L.push(`- ACs: ${inc.doneAcs}/${inc.totalAcs} checked`);
173
+ if (inc.acSyncEvents.length > 0) {
174
+ L.push('- AC/task drift (recent sync events):');
175
+ for (const ev of inc.acSyncEvents)
176
+ L.push(` - ${ev}`);
177
+ }
178
+ }
179
+ else {
180
+ L.push('_No increment task/AC state available._');
181
+ }
182
+ L.push('');
183
+ // ── Key Decisions & Gotchas ───────────────────────────────────────────────
184
+ L.push('## Key Decisions & Gotchas');
185
+ L.push('');
186
+ if (input.decisions.length > 0) {
187
+ for (const d of input.decisions)
188
+ L.push(`- ${d}`);
189
+ }
190
+ else {
191
+ L.push('_No decisions recorded._');
192
+ }
193
+ if (input.gotcha) {
194
+ L.push('');
195
+ L.push(`**Gotcha:** ${input.gotcha}`);
196
+ }
197
+ if (input.ambient && (input.ambient.testMode || input.ambient.coverageTarget != null || input.ambient.wipLimit != null)) {
198
+ L.push('');
199
+ L.push('**Ambient rules (config.json):**');
200
+ if (input.ambient.testMode)
201
+ L.push(`- Test mode: ${input.ambient.testMode}`);
202
+ if (input.ambient.coverageTarget != null)
203
+ L.push(`- Coverage target: ${input.ambient.coverageTarget}%`);
204
+ if (input.ambient.wipLimit != null)
205
+ L.push(`- WIP limit: ${input.ambient.wipLimit}`);
206
+ }
207
+ L.push('');
208
+ // ── Files Touched ─────────────────────────────────────────────────────────
209
+ L.push('## Files Touched');
210
+ L.push('');
211
+ if (input.git.hasUncommittedChanges) {
212
+ L.push('**UNCOMMITTED** — commit, stash, or keep editing BEFORE doing anything destructive.');
213
+ L.push('');
214
+ L.push('```');
215
+ L.push(input.git.statusPorcelain || '(no porcelain output)');
216
+ L.push('```');
217
+ if (input.git.diffStat) {
218
+ L.push('');
219
+ L.push('```');
220
+ L.push(input.git.diffStat);
221
+ L.push('```');
222
+ }
223
+ L.push('');
224
+ L.push(`Full uncommitted diff: \`${input.diffPath}\` — read it or run ` +
225
+ '`git apply --check` against it to see the exact edits.');
226
+ }
227
+ else if (input.git.isGitRepo) {
228
+ L.push('_Working tree clean — no uncommitted edits._');
229
+ }
230
+ else {
231
+ L.push('_Not a git repository — no diff captured._');
232
+ }
233
+ L.push('');
234
+ // ── Exact Next Steps ──────────────────────────────────────────────────────
235
+ L.push('## Exact Next Steps');
236
+ L.push('');
237
+ if (input.next) {
238
+ L.push(input.next);
239
+ }
240
+ else if (input.increment?.nextTask) {
241
+ L.push(`Continue with: ${input.increment.nextTask}`);
242
+ }
243
+ else {
244
+ L.push('_No explicit next step recorded — review the summary above._');
245
+ }
246
+ L.push('');
247
+ // ── How To Resume (+ per-tool matrix) ─────────────────────────────────────
248
+ L.push(renderResumeMatrix());
249
+ L.push('');
250
+ // ── Redaction ─────────────────────────────────────────────────────────────
251
+ L.push('## Redaction');
252
+ L.push('');
253
+ L.push(fmtCount(input.redactionCounts));
254
+ L.push('');
255
+ L.push('_Scrubbing is heuristic (regex baseline). An empty redaction list is NOT a ' +
256
+ 'guarantee this file is clean — review before sharing or committing._');
257
+ L.push('');
258
+ // ── Footer marker ─────────────────────────────────────────────────────────
259
+ L.push('---');
260
+ L.push(`<!-- ${DOC_FORMAT_MARKER} -->`);
261
+ return L.join('\n');
262
+ }
263
+ /**
264
+ * Render the copy-paste resume prompt.
265
+ *
266
+ * Default (file-reachable) mode points the next agent at the doc path. The
267
+ * `inline` mode embeds the FULL doc body between BEGIN/END markers for
268
+ * cross-machine resume where the file is unreachable (AC-US5-04).
269
+ */
270
+ export function renderPastePrompt(input, opts = {}) {
271
+ const P = [];
272
+ if (opts.inline) {
273
+ P.push('Resume my work using the self-contained handoff below.');
274
+ P.push('This is a full handoff doc — treat everything between the markers as ground truth. ' +
275
+ 'If it references a `.diff` path that does not exist on this machine, ask me to paste the diff.');
276
+ P.push('');
277
+ P.push(INLINE_BEGIN_MARKER);
278
+ P.push(renderHandoffDoc(input));
279
+ P.push(INLINE_END_MARKER);
280
+ }
281
+ else {
282
+ P.push(`Resume my work. Read the handoff doc at: ${input.docPath}`);
283
+ P.push('If that path does NOT exist on this machine, STOP and ask me to paste the handoff — ' +
284
+ 'do not improvise context.');
285
+ P.push(`The exact uncommitted edits are in: ${input.diffPath}`);
286
+ if (input.increment) {
287
+ P.push(`Active increment: ${input.increment.id}. Pick up at the next pending task.`);
288
+ }
289
+ }
290
+ return P.join('\n');
291
+ }
292
+ //# sourceMappingURL=handoff-doc-format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoff-doc-format.js","sourceRoot":"","sources":["../../../../src/core/session/handoff-doc-format.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Handoff Git State Capture
3
+ *
4
+ * Captures the cheap, deterministic git facts the handoff doc needs (branch,
5
+ * short sha, porcelain status, diff --stat) AND dumps the FULL working-tree +
6
+ * staged diff to a sibling `.diff` file. The full diff is captured for free via
7
+ * git — no LLM tokens are spent — and is the key in-flight-fidelity artifact:
8
+ * it lets the resuming agent see exactly what was changed but not yet committed.
9
+ *
10
+ * In a non-git directory every field degrades to empty and no error is thrown,
11
+ * so the non-SpecWeave fallback path still works on plain folders.
12
+ *
13
+ * Part of increment 0867: Cross-Tool Work Handoff (AC-US4-01..04).
14
+ *
15
+ * @module core/session/handoff-git-state
16
+ */
17
+ /**
18
+ * Captured git state for the handoff doc.
19
+ */
20
+ export interface GitState {
21
+ /** Whether `repoRoot` is inside a git work tree at all. */
22
+ isGitRepo: boolean;
23
+ /** Current branch name (empty if detached or non-git). */
24
+ branch: string;
25
+ /** Short commit SHA of HEAD (empty if no commits / non-git). */
26
+ shortSha: string;
27
+ /** `git status --porcelain` output (empty when clean or non-git). */
28
+ statusPorcelain: string;
29
+ /** Combined `git diff --stat` (working) + `git diff --cached --stat` (staged). */
30
+ diffStat: string;
31
+ /** True when there are working-tree or staged changes. */
32
+ hasUncommittedChanges: boolean;
33
+ }
34
+ /**
35
+ * Capture git state for `repoRoot` and dump the full uncommitted diff to
36
+ * `diffOutputPath`.
37
+ *
38
+ * The diff file always contains `git diff HEAD` (working-tree vs last commit)
39
+ * concatenated with `git diff --cached` (staged vs HEAD). When the repo is
40
+ * clean — or before the first commit — the file is written empty so callers can
41
+ * unconditionally link to it.
42
+ *
43
+ * @param repoRoot - Absolute path to the candidate repository root.
44
+ * @param diffOutputPath - Absolute path where the full diff is written.
45
+ * @returns The captured {@link GitState}; all-empty + `isGitRepo:false` when
46
+ * `repoRoot` is not a git work tree (no throw).
47
+ */
48
+ export declare function captureGitState(repoRoot: string, diffOutputPath: string): GitState;
49
+ //# sourceMappingURL=handoff-git-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoff-git-state.d.ts","sourceRoot":"","sources":["../../../../src/core/session/handoff-git-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,2DAA2D;IAC3D,SAAS,EAAE,OAAO,CAAC;IACnB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,eAAe,EAAE,MAAM,CAAC;IACxB,kFAAkF;IAClF,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,qBAAqB,EAAE,OAAO,CAAC;CAChC;AAqBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,QAAQ,CA6DlF"}
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Handoff Git State Capture
3
+ *
4
+ * Captures the cheap, deterministic git facts the handoff doc needs (branch,
5
+ * short sha, porcelain status, diff --stat) AND dumps the FULL working-tree +
6
+ * staged diff to a sibling `.diff` file. The full diff is captured for free via
7
+ * git — no LLM tokens are spent — and is the key in-flight-fidelity artifact:
8
+ * it lets the resuming agent see exactly what was changed but not yet committed.
9
+ *
10
+ * In a non-git directory every field degrades to empty and no error is thrown,
11
+ * so the non-SpecWeave fallback path still works on plain folders.
12
+ *
13
+ * Part of increment 0867: Cross-Tool Work Handoff (AC-US4-01..04).
14
+ *
15
+ * @module core/session/handoff-git-state
16
+ */
17
+ import { execFileSync } from 'child_process';
18
+ import * as fs from 'fs';
19
+ import * as path from 'path';
20
+ /**
21
+ * Run a git command, returning trimmed stdout or `null` on any failure.
22
+ *
23
+ * Uses execFileSync with an argument array (no shell) so paths and refs are
24
+ * never re-interpreted by a shell.
25
+ */
26
+ function git(repoRoot, args) {
27
+ try {
28
+ return execFileSync('git', args, {
29
+ cwd: repoRoot,
30
+ encoding: 'utf-8',
31
+ stdio: ['ignore', 'pipe', 'ignore'],
32
+ maxBuffer: 64 * 1024 * 1024, // diffs can be large
33
+ }).trim();
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ /**
40
+ * Capture git state for `repoRoot` and dump the full uncommitted diff to
41
+ * `diffOutputPath`.
42
+ *
43
+ * The diff file always contains `git diff HEAD` (working-tree vs last commit)
44
+ * concatenated with `git diff --cached` (staged vs HEAD). When the repo is
45
+ * clean — or before the first commit — the file is written empty so callers can
46
+ * unconditionally link to it.
47
+ *
48
+ * @param repoRoot - Absolute path to the candidate repository root.
49
+ * @param diffOutputPath - Absolute path where the full diff is written.
50
+ * @returns The captured {@link GitState}; all-empty + `isGitRepo:false` when
51
+ * `repoRoot` is not a git work tree (no throw).
52
+ */
53
+ export function captureGitState(repoRoot, diffOutputPath) {
54
+ const empty = {
55
+ isGitRepo: false,
56
+ branch: '',
57
+ shortSha: '',
58
+ statusPorcelain: '',
59
+ diffStat: '',
60
+ hasUncommittedChanges: false,
61
+ };
62
+ const insideWorkTree = git(repoRoot, ['rev-parse', '--is-inside-work-tree']);
63
+ if (insideWorkTree !== 'true') {
64
+ // Still write an empty diff file so the doc's link target exists.
65
+ safeWriteDiff(diffOutputPath, '');
66
+ return empty;
67
+ }
68
+ const branch = git(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD']) ?? '';
69
+ const shortSha = git(repoRoot, ['rev-parse', '--short', 'HEAD']) ?? '';
70
+ const statusPorcelain = git(repoRoot, ['status', '--porcelain']) ?? '';
71
+ // Intent-to-add every untracked file so `git diff` includes its FULL body,
72
+ // not just a `??` porcelain line. Without this, brand-new uncommitted files —
73
+ // the most common in-flight-work case — would be captured as a filename only,
74
+ // breaking the spec's "exact uncommitted edits" promise (AC-US4-01/02).
75
+ // `add -N` records intent only (no content staged) but it DOES add an index
76
+ // entry, so we MUST revert it afterward to leave the user's real index exactly
77
+ // as we found it — a handoff is a read-only capture and must not silently
78
+ // change `git status`, `git stash`, or `git commit -a` behavior.
79
+ const intentAdded = stageIntentToAddUntracked(repoRoot, statusPorcelain);
80
+ // `git diff HEAD` covers working-tree vs HEAD; if there is no HEAD yet
81
+ // (no commits), fall back to plain `git diff` so brand-new repos still work.
82
+ const hasHead = shortSha !== '';
83
+ const workingDiff = (hasHead ? git(repoRoot, ['diff', 'HEAD']) : git(repoRoot, ['diff'])) ?? '';
84
+ const stagedDiff = (hasHead ? git(repoRoot, ['diff', '--cached']) : '') ?? '';
85
+ const workingStat = (hasHead ? git(repoRoot, ['diff', '--stat', 'HEAD']) : git(repoRoot, ['diff', '--stat'])) ?? '';
86
+ const stagedStat = (hasHead ? git(repoRoot, ['diff', '--cached', '--stat']) : '') ?? '';
87
+ // Restore the index: drop the intent-to-add entries we created so the user's
88
+ // staging area is byte-identical to its pre-handoff state.
89
+ unstageIntentToAdd(repoRoot, hasHead, intentAdded);
90
+ const diffStat = [workingStat, stagedStat].filter(Boolean).join('\n');
91
+ const fullDiff = [workingDiff, stagedDiff].filter(Boolean).join('\n');
92
+ safeWriteDiff(diffOutputPath, fullDiff);
93
+ const hasUncommittedChanges = statusPorcelain.length > 0;
94
+ return {
95
+ isGitRepo: true,
96
+ branch,
97
+ shortSha,
98
+ statusPorcelain,
99
+ diffStat,
100
+ hasUncommittedChanges,
101
+ };
102
+ }
103
+ /**
104
+ * Intent-to-add every untracked file from a porcelain status so its content
105
+ * shows up in `git diff`. Parses `??`-prefixed porcelain lines and runs a
106
+ * single `git add -N -- <paths...>`. Never throws — a failure here only means
107
+ * untracked bodies are omitted, which must not abort the handoff.
108
+ *
109
+ * `add -N` (intent-to-add) records that a path will be tracked but stages NO
110
+ * content. It DOES create an index entry, so the caller MUST pass the returned
111
+ * paths to {@link unstageIntentToAdd} once the diff has been captured to leave
112
+ * the user's real index untouched.
113
+ *
114
+ * @returns The untracked paths that were intent-to-added (empty if none).
115
+ */
116
+ function stageIntentToAddUntracked(repoRoot, statusPorcelain) {
117
+ if (!statusPorcelain)
118
+ return [];
119
+ const untracked = statusPorcelain
120
+ .split('\n')
121
+ .filter((line) => line.startsWith('??'))
122
+ // Porcelain format is `XY <path>`; for untracked the path starts at col 3.
123
+ .map((line) => line.slice(3).trim())
124
+ .filter(Boolean);
125
+ if (untracked.length === 0)
126
+ return [];
127
+ // `--` guards against any path that looks like a flag.
128
+ git(repoRoot, ['add', '-N', '--', ...untracked]);
129
+ return untracked;
130
+ }
131
+ /**
132
+ * Revert the intent-to-add entries created by {@link stageIntentToAddUntracked}
133
+ * so the user's index is restored to its pre-handoff state. After this the
134
+ * affected files are untracked (`??`) again, exactly as before the capture.
135
+ *
136
+ * `git reset -- <paths>` clears the index entries when a HEAD exists. Before the
137
+ * first commit there is no HEAD, so `git reset` would fail; `git rm --cached
138
+ * --force -- <paths>` removes the intent entries in that case. Never throws — a
139
+ * failed revert must not abort the handoff (worst case: the index keeps the
140
+ * intent entries, which is the pre-fix behavior).
141
+ */
142
+ function unstageIntentToAdd(repoRoot, hasHead, paths) {
143
+ if (paths.length === 0)
144
+ return;
145
+ if (hasHead) {
146
+ git(repoRoot, ['reset', '--quiet', '--', ...paths]);
147
+ }
148
+ else {
149
+ git(repoRoot, ['rm', '--cached', '--force', '--quiet', '--', ...paths]);
150
+ }
151
+ }
152
+ /**
153
+ * Write the diff file, creating the parent dir; never throws.
154
+ */
155
+ function safeWriteDiff(diffOutputPath, content) {
156
+ try {
157
+ fs.mkdirSync(path.dirname(diffOutputPath), { recursive: true });
158
+ fs.writeFileSync(diffOutputPath, content, 'utf-8');
159
+ }
160
+ catch {
161
+ // Best-effort: a failed diff dump must not abort the handoff.
162
+ }
163
+ }
164
+ //# sourceMappingURL=handoff-git-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoff-git-state.js","sourceRoot":"","sources":["../../../../src/core/session/handoff-git-state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
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
+ * A single secret-detection pattern.
19
+ */
20
+ export interface SecretPattern {
21
+ /** Stable type slug used in the `[REDACTED-<type>]` marker + counts map. */
22
+ type: string;
23
+ /** Regex matching the secret. MUST use the global flag for replace + count. */
24
+ regex: RegExp;
25
+ }
26
+ /**
27
+ * The 12 redaction patterns (AC-US6-01).
28
+ *
29
+ * Patterns are intentionally conservative: they match the well-known token
30
+ * prefixes plus enough trailing characters to avoid masking the prefix alone.
31
+ * Assignment-style secrets (`password=`, `api_key=`) capture the value up to
32
+ * whitespace.
33
+ */
34
+ export declare const SECRET_PATTERNS: readonly SecretPattern[];
35
+ /**
36
+ * Result of a scrub: the redacted text plus per-pattern occurrence counts.
37
+ */
38
+ export interface ScrubResult {
39
+ /** Text with every matched secret replaced by `[REDACTED-<type>]`. */
40
+ scrubbed: string;
41
+ /** Map of pattern `type` → number of redactions (only non-zero entries). */
42
+ counts: Record<string, number>;
43
+ }
44
+ /**
45
+ * Scrub secrets from a block of text.
46
+ *
47
+ * Runs each declared pattern in order. Patterns are applied sequentially over
48
+ * the progressively-scrubbed text, so an already-redacted span cannot be
49
+ * matched a second time by a later pattern.
50
+ *
51
+ * @param text - Free-text or diff content to scrub.
52
+ * @returns Scrubbed text + per-pattern counts (only patterns that fired).
53
+ */
54
+ export declare function scrubSecrets(text: string): ScrubResult;
55
+ /**
56
+ * Total number of redactions across all patterns.
57
+ */
58
+ export declare function totalRedactions(counts: Record<string, number>): number;
59
+ //# sourceMappingURL=handoff-secret-scrub.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoff-secret-scrub.d.ts","sourceRoot":"","sources":["../../../../src/core/session/handoff-secret-scrub.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,aAAa,EAa1C,CAAC;AAEX;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAkBtD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAEtE"}