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,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"}
|