toga-ai 1.0.968 → 1.0.970
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/.claude/settings.json +19 -0
- package/knowledge/1.0/apps/tools/architecture.md +12 -1
- package/knowledge/1.0/apps/tools/features/central-ids.md +2 -2
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/features/page-meta-context-field-settings.md +3 -2
- package/knowledge/2.0/apps/_underscore/features/surface-overrides-uuid-rekey.md +5 -4
- package/knowledge/2.0/apps/_underscore/features/surface-resolver.md +18 -1
- package/knowledge/2.0/apps/saml/features/acs-assertion-trust-gaps.md +38 -1
- package/knowledge/2.0/apps/toga2-commerce/features/view-as-session-tokens.md +3 -1
- package/package.json +1 -1
- package/rules/common/git-workflow.md +14 -10
- package/scripts/ci/pr-risk.js +262 -0
- package/scripts/hooks/session-files.js +144 -0
- package/skills/commit/SKILL.md +124 -0
- package/skills/commit/scripts/commit-helper.js +469 -0
- package/skills/work-ticket/SKILL.md +3 -3
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* session-files.js — remembers which files THIS Claude Code session edited, per git repo.
|
|
3
|
+
*
|
|
4
|
+
* PostToolUse (Edit|Write|MultiEdit|NotebookEdit): appends one JSON line to
|
|
5
|
+
* <gitdir>/claude-sessions/<sessionId>.jsonl — read later by skills/commit (commit-helper.js).
|
|
6
|
+
* Silent on stdout. No child processes (fires on every edit — must stay fast).
|
|
7
|
+
* UserPromptSubmit: when the prompt starts with "/commit", prints the session id + transcript
|
|
8
|
+
* path so the /commit skill knows which list is its own.
|
|
9
|
+
*
|
|
10
|
+
* Never denies, never crashes, always exits 0. Disable: TOGA_SESSION_FILES_DISABLED=1.
|
|
11
|
+
*/
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const PRUNE_AFTER_DAYS = 14;
|
|
16
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
17
|
+
const MAX_SESSION_ID_LENGTH = 64;
|
|
18
|
+
const SESSIONS_DIR_NAME = 'claude-sessions';
|
|
19
|
+
const COMMIT_PROMPT_RE = /^\/(?:[\w-]+:)?commit(\s|$)/;
|
|
20
|
+
|
|
21
|
+
/* A closed pipe (EPIPE) on stdout must never crash the hook. */
|
|
22
|
+
process.stdout.on('error', () => {});
|
|
23
|
+
|
|
24
|
+
function readInput() {
|
|
25
|
+
try {
|
|
26
|
+
const raw = fs.readFileSync(0, 'utf8');
|
|
27
|
+
if (!raw || !raw.trim()) return null;
|
|
28
|
+
return JSON.parse(raw);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function safeSessionId(id) {
|
|
35
|
+
return String(id || '').replace(/[^A-Za-z0-9_-]/g, '').slice(0, MAX_SESSION_ID_LENGTH);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Walk up from `start` looking for .git. Returns { repoRoot, gitDir } or null. */
|
|
39
|
+
function findRepo(start) {
|
|
40
|
+
let dir = start;
|
|
41
|
+
for (;;) {
|
|
42
|
+
const dotGit = path.join(dir, '.git');
|
|
43
|
+
let st = null;
|
|
44
|
+
try { st = fs.statSync(dotGit); } catch (e) { st = null; }
|
|
45
|
+
if (st && st.isDirectory()) return { repoRoot: dir, gitDir: dotGit };
|
|
46
|
+
if (st && st.isFile()) {
|
|
47
|
+
try {
|
|
48
|
+
const m = fs.readFileSync(dotGit, 'utf8').match(/^gitdir:\s*(.+)\s*$/m);
|
|
49
|
+
if (m) return { repoRoot: dir, gitDir: path.resolve(dir, m[1].trim()) };
|
|
50
|
+
} catch (e) { /* unreadable .git file — keep walking */ }
|
|
51
|
+
}
|
|
52
|
+
const parent = path.dirname(dir);
|
|
53
|
+
if (parent === dir) return null;
|
|
54
|
+
dir = parent;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hasHeadFile(gitDir) {
|
|
59
|
+
try { return fs.statSync(path.join(gitDir, 'HEAD')).isFile(); } catch (e) { return false; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readBranch(gitDir) {
|
|
63
|
+
try {
|
|
64
|
+
const head = fs.readFileSync(path.join(gitDir, 'HEAD'), 'utf8').trim();
|
|
65
|
+
const m = head.match(/^ref:\s*refs\/heads\/(.+)$/);
|
|
66
|
+
if (m) return m[1];
|
|
67
|
+
return head.slice(0, 7);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isInside(child, parent) {
|
|
74
|
+
const rel = path.relative(parent, child);
|
|
75
|
+
return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function pruneOld(dir, keepFile) {
|
|
79
|
+
const cutoff = Date.now() - PRUNE_AFTER_DAYS * MS_PER_DAY;
|
|
80
|
+
let names = [];
|
|
81
|
+
try { names = fs.readdirSync(dir); } catch (e) { return; }
|
|
82
|
+
for (const name of names) {
|
|
83
|
+
const full = path.join(dir, name);
|
|
84
|
+
if (full === keepFile || !name.endsWith('.jsonl')) continue;
|
|
85
|
+
try {
|
|
86
|
+
if (fs.statSync(full).mtimeMs < cutoff) fs.unlinkSync(full);
|
|
87
|
+
} catch (e) { /* another session may have removed it — fine */ }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function handlePostToolUse(input) {
|
|
92
|
+
const sessionId = safeSessionId(input.session_id);
|
|
93
|
+
if (!sessionId) return;
|
|
94
|
+
const ti = input.tool_input || {};
|
|
95
|
+
const rawPath = ti.file_path || ti.notebook_path;
|
|
96
|
+
if (!rawPath) return;
|
|
97
|
+
|
|
98
|
+
const filePath = path.resolve(input.cwd || process.cwd(), String(rawPath));
|
|
99
|
+
const repo = findRepo(path.dirname(filePath));
|
|
100
|
+
if (!repo) return;
|
|
101
|
+
if (!hasHeadFile(repo.gitDir)) return; /* not a real git dir — write nothing */
|
|
102
|
+
if (isInside(filePath, repo.gitDir) || isInside(filePath, path.join(repo.repoRoot, '.git'))) return;
|
|
103
|
+
|
|
104
|
+
const sessionsDir = path.join(repo.gitDir, SESSIONS_DIR_NAME);
|
|
105
|
+
const listFile = path.join(sessionsDir, sessionId + '.jsonl');
|
|
106
|
+
const isNew = !fs.existsSync(listFile);
|
|
107
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
108
|
+
|
|
109
|
+
const entry = {
|
|
110
|
+
ts: new Date().toISOString(),
|
|
111
|
+
sessionId: String(input.session_id),
|
|
112
|
+
transcriptPath: input.transcript_path || null,
|
|
113
|
+
repoRoot: repo.repoRoot,
|
|
114
|
+
branch: readBranch(repo.gitDir),
|
|
115
|
+
file: path.relative(repo.repoRoot, filePath).split(path.sep).join('/'),
|
|
116
|
+
tool: input.tool_name || null,
|
|
117
|
+
};
|
|
118
|
+
if (input.agent_id) entry.agentId = input.agent_id;
|
|
119
|
+
fs.appendFileSync(listFile, JSON.stringify(entry) + '\n');
|
|
120
|
+
|
|
121
|
+
if (isNew) pruneOld(sessionsDir, listFile);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function handleUserPromptSubmit(input) {
|
|
125
|
+
const prompt = String(input.prompt || '').trim();
|
|
126
|
+
if (!COMMIT_PROMPT_RE.test(prompt)) return;
|
|
127
|
+
process.stdout.write('TOGA session id: ' + (input.session_id || 'unknown') +
|
|
128
|
+
' (transcript: ' + (input.transcript_path || 'unknown') + ')\n');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function main() {
|
|
132
|
+
if (process.env.TOGA_SESSION_FILES_DISABLED === '1') return;
|
|
133
|
+
const input = readInput();
|
|
134
|
+
if (!input) return;
|
|
135
|
+
if (input.hook_event_name === 'UserPromptSubmit') return handleUserPromptSubmit(input);
|
|
136
|
+
if (input.hook_event_name === 'PostToolUse') return handlePostToolUse(input);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
main();
|
|
141
|
+
} catch (e) {
|
|
142
|
+
/* Never block or break a session over bookkeeping. */
|
|
143
|
+
}
|
|
144
|
+
process.exitCode = 0; /* not process.exit() — let stdout flush */
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: commit
|
|
3
|
+
description: Commit ONLY the files this Claude Code session changed, in every repo it touched, and write the session's notes (why the task exists, what was found, approach, turned-down options, who decided) into the commit message so the PR reviewer can build the PR summary from it. Safe when several sessions work on the same branch in the same checkout. Never pushes. Trigger on "/commit", "commit this", "commit my changes".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /commit — commit this session's files, with the session's notes
|
|
7
|
+
|
|
8
|
+
Helper: `node "$CLAUDE_PROJECT_DIR/.claude/skills/commit/scripts/commit-helper.js" <command>`
|
|
9
|
+
(below: `helper <command>`). It prints JSON. The edit lists come from the
|
|
10
|
+
`session-files.js` hook, stored in `<repo>/.git/claude-sessions/<sessionId>.jsonl`.
|
|
11
|
+
|
|
12
|
+
## Step 0 — Rules
|
|
13
|
+
|
|
14
|
+
- Running `/commit` IS the developer's explicit commit authorization — **for these commits only**.
|
|
15
|
+
- Never push. Never `git add -A` or `git add .`. Never `--no-verify`. Never `--amend`.
|
|
16
|
+
Never switch branches. Never commit a file the developer did not approve.
|
|
17
|
+
|
|
18
|
+
## Step 1 — Find your own session id
|
|
19
|
+
|
|
20
|
+
- Look in context for the line `TOGA session id: <id> (transcript: <path>)` — the hook adds it
|
|
21
|
+
when the prompt starts with `/commit`. Use that id and transcript path.
|
|
22
|
+
- If it is missing: run `helper sessions`. Show the developer each list (repo, branch, file
|
|
23
|
+
count, first/last time, first message). Ask which one is theirs. Use its `transcriptPath`.
|
|
24
|
+
|
|
25
|
+
## Step 2 — See what will be committed
|
|
26
|
+
|
|
27
|
+
Run `helper status --session=<id>`. For each repo, tell the developer in plain words:
|
|
28
|
+
|
|
29
|
+
1. **Will commit:** the `toCommit` files.
|
|
30
|
+
2. **Other changed files, not from this session:** the `otherChanged` files (hand edits,
|
|
31
|
+
lockfiles, generated code, files changed by shell commands). Include them **only if the
|
|
32
|
+
developer says so**. Note any included ones under "Manual changes".
|
|
33
|
+
3. **Overlap warning** for each `overlaps` file: "`X` was also changed by another session —
|
|
34
|
+
committing it includes their changes too." Ask before including it.
|
|
35
|
+
4. **Branch mismatch:** `branchMismatch` files were last edited on another branch
|
|
36
|
+
(`entryBranch`). They are NOT in `toCommit`. If the list is not empty, **stop** for that
|
|
37
|
+
repo: say which files were edited on which branch, and ask the developer to switch
|
|
38
|
+
branches themselves.
|
|
39
|
+
5. **Possibly yours from before a resume:** list `sameBranchOtherSessions` (time, file count).
|
|
40
|
+
If the developer says one is theirs, run `status` with that id too and merge the lists.
|
|
41
|
+
6. **Ticket:** use `ticket` from the branch name. If it is `null`, ask the developer for it.
|
|
42
|
+
|
|
43
|
+
`alreadyClean` files have no change right now (committed, reverted, or stashed). Do not commit
|
|
44
|
+
them. They stay in the list, so a `git stash pop` brings them back into `toCommit`.
|
|
45
|
+
|
|
46
|
+
`repos[]` entries with an `error` field could not be read — tell the developer the error.
|
|
47
|
+
|
|
48
|
+
## Step 3 — Checks before committing
|
|
49
|
+
|
|
50
|
+
On the files you will commit:
|
|
51
|
+
|
|
52
|
+
1. `php -l <file>` on every PHP file. If `php` is not on PATH, say so — do not skip silently.
|
|
53
|
+
2. After staging, read `git -C <repo> diff --cached` and look for:
|
|
54
|
+
- secrets: keys, tokens, passwords, connection strings, `.env` files;
|
|
55
|
+
- absolute local paths (`C:\`, `D:\`, `/Users/`);
|
|
56
|
+
- commented-out code.
|
|
57
|
+
3. Confirm the branch is the ticket branch.
|
|
58
|
+
|
|
59
|
+
Fix what you can. If you cannot, unstage (`git -C <repo> restore --staged -- <files>`), stop,
|
|
60
|
+
and tell the developer.
|
|
61
|
+
|
|
62
|
+
## Step 4 — Get the developer's own words
|
|
63
|
+
|
|
64
|
+
Run `helper devwords --transcript=<path>`. Use it to:
|
|
65
|
+
|
|
66
|
+
- fill "Developer's words" with a short real quote;
|
|
67
|
+
- decide honestly who made each decision. A decision is the **developer's** only if the
|
|
68
|
+
developer said it or approved it in their own message. Otherwise it is **Claude alone**.
|
|
69
|
+
|
|
70
|
+
If you still do not know why the task exists, ask the developer once, in one plain question.
|
|
71
|
+
|
|
72
|
+
## Step 5 — Write the message and commit, per repo
|
|
73
|
+
|
|
74
|
+
Subject rules (api2 CI checks this; use it in every repo): `<TICKET>: <Subject>` — ticket from
|
|
75
|
+
the branch (`^[A-Z]+-\d+`), subject starts with a capital letter, imperative ("Use", "Add",
|
|
76
|
+
"Fix" — not "Used/Added/Fixed"), max 80 characters, no period at the end, more than one word.
|
|
77
|
+
|
|
78
|
+
Write the message to a temp file **outside every repo** — the OS temp dir (`$TMPDIR`, `$TEMP`,
|
|
79
|
+
or `os.tmpdir()`), named like `toga-commit-<session id>-<repo name>.txt`. Never inside the repo,
|
|
80
|
+
so it can never be staged. Use this format EXACTLY:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
TRUE-79868: Use client default tax code when invoice tax code is blank
|
|
84
|
+
|
|
85
|
+
Why this exists: <problem + scope in 1-3 plain lines — why the ticket exists, what is and isn't in scope>
|
|
86
|
+
Developer's words: "<short quote of how the developer described the task>"
|
|
87
|
+
Found: <what was found, 1-3 lines>
|
|
88
|
+
Approach: <what this commit does in THIS repo and why, 1-3 lines>
|
|
89
|
+
Turned down: <options considered and rejected, and why — or "none">
|
|
90
|
+
Decisions:
|
|
91
|
+
- <decision> (decided by: developer | Claude alone)
|
|
92
|
+
Manual changes: <files the developer changed by hand and asked to include — or "none">
|
|
93
|
+
|
|
94
|
+
Claude-Notes: v1
|
|
95
|
+
Claude-Session: <session id>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- Same "Why this exists" and "Developer's words" in every repo of the same ticket.
|
|
99
|
+
"Approach" and "Decisions" are specific to each repo.
|
|
100
|
+
- 5-15 body lines. Plain everyday words. No jargon. No arrow chains.
|
|
101
|
+
- Small follow-up commit on the same ticket: keep every heading; one line each is fine.
|
|
102
|
+
|
|
103
|
+
`devwords` already replaces token shapes and `password=...` values with `[redacted]`. Still,
|
|
104
|
+
before you commit, read the finished message and check it for:
|
|
105
|
+
|
|
106
|
+
- secrets: API keys (`sk-ant-`, `ghp_`, `github_pat_`, `AKIA`), private keys, passwords,
|
|
107
|
+
tokens, connection strings — anything that looks like one, even `[redacted]` leftovers;
|
|
108
|
+
- absolute local paths (`C:\`, `D:\`, `/Users/`, `/home/`).
|
|
109
|
+
|
|
110
|
+
Remove or reword anything you find. The message goes into git history for good.
|
|
111
|
+
|
|
112
|
+
Then, per repo:
|
|
113
|
+
|
|
114
|
+
1. `git -C <repo> add -- <exact files>`
|
|
115
|
+
2. `git -C <repo> commit -F <temp file>`
|
|
116
|
+
3. Delete the temp file (also when the commit fails).
|
|
117
|
+
|
|
118
|
+
## Step 6 — After each commit
|
|
119
|
+
|
|
120
|
+
1. `helper forget --session=<id> --repo=<repo> --files=<committed files, comma-separated>`
|
|
121
|
+
2. Report per repo: short hash, subject, file count.
|
|
122
|
+
3. Say: "Push when ready — /commit never pushes."
|
|
123
|
+
4. Say: changes made by hand outside Claude can still be committed the normal way in VS Code;
|
|
124
|
+
the PR reviewer will show those as manual changes.
|