toga-ai 1.0.168 → 1.0.170
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/knowledge/CONVENTIONS.md +39 -4
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/rate/INDEX.md +1 -0
- package/knowledge/clients/rate/features/service-card-entitlements.md +103 -0
- package/knowledge.js +227 -1
- package/package.json +1 -1
- package/scripts/hooks/session-start.js +38 -0
- package/skills/session-resume/SKILL.md +47 -28
- package/skills/session-save/SKILL.md +71 -25
- package/skills/team-skills/SKILL.md +50 -0
package/knowledge/CONVENTIONS.md
CHANGED
|
@@ -34,10 +34,12 @@ knowledge/
|
|
|
34
34
|
│ └── standards/*.md # 2.0 coding standards (_underscore)
|
|
35
35
|
├── standalone/
|
|
36
36
|
│ └── apps/<repo>/architecture.md + features/*.md # non-PHP apps (e.g. togatech); no core
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
├── clients/<client>/
|
|
38
|
+
│ ├── profile.md
|
|
39
|
+
│ ├── features/*.md # client-specific feature overrides (link back to apps/)
|
|
40
|
+
│ └── workflows/*.md # client business processes
|
|
41
|
+
└── sessions/ # team-shared resumable sessions (NOT knowledge docs)
|
|
42
|
+
└── YYYY-MM-DD-<slug>-<author>.md
|
|
41
43
|
```
|
|
42
44
|
|
|
43
45
|
**App folders are keyed by REPO name** (`worker2`, `_underscore`, `api2`, `library`),
|
|
@@ -211,6 +213,39 @@ Never hand-edited. `node knowledge.js index` regenerates the master `INDEX.md`,
|
|
|
211
213
|
`<fw>/apps/<repo>/INDEX.md`, and each `clients/<client>/INDEX.md` from frontmatter, so
|
|
212
214
|
they can never drift from the docs.
|
|
213
215
|
|
|
216
|
+
## Team-shared sessions (`knowledge/sessions/`, `type: session`)
|
|
217
|
+
|
|
218
|
+
Sessions are **resumable work state**, not knowledge docs — they let one developer save an
|
|
219
|
+
in-progress task and another (or future-you, on another machine) resume it. They ride the
|
|
220
|
+
same git rails as `capture` (rebase-before-push to `_main`), but are managed by the
|
|
221
|
+
`session-save` / `session-resume` skills and the `knowledge.js session-*` commands, **not**
|
|
222
|
+
by `capture`.
|
|
223
|
+
|
|
224
|
+
- **File:** `knowledge/sessions/YYYY-MM-DD-<slug>-<author>.md`. The `<author>` suffix lets
|
|
225
|
+
two developers reuse a slug without collision.
|
|
226
|
+
- **Frontmatter** (its own schema — distinct from the doc frontmatter above):
|
|
227
|
+
```yaml
|
|
228
|
+
type: session
|
|
229
|
+
slug: <kebab-slug>
|
|
230
|
+
title: <short human title>
|
|
231
|
+
author: <kb-username>
|
|
232
|
+
repos: [<repo>, …] # repos touched (free-form; NOT checked against registry)
|
|
233
|
+
framework: "1.0" | "2.0" | "both" | "standalone"
|
|
234
|
+
client: <slug> | shared
|
|
235
|
+
status: active | stale # stale = abandoned / safe to delete
|
|
236
|
+
created: YYYY-MM-DD
|
|
237
|
+
updated: YYYY-MM-DD
|
|
238
|
+
```
|
|
239
|
+
- **Not validated as docs.** `allDocs()` walks only `1.0/`, `2.0/`, `standalone/`, and
|
|
240
|
+
`clients/`, so `validate` and `index` deliberately ignore `sessions/`. `session-save`
|
|
241
|
+
runs its own lightweight frontmatter check and the **same secret scan** before pushing —
|
|
242
|
+
never paste credentials into a session body.
|
|
243
|
+
- **Commands:** `session-save --file=`, `session-list [--stale=N --mine --json]`,
|
|
244
|
+
`session-show <id>`, `session-delete <id>`. List/show/delete read from `origin/_main` after
|
|
245
|
+
a `git fetch`, so they reflect the freshest team state without touching the working tree.
|
|
246
|
+
- **Cleanup is manual.** `session-list` flags sessions older than 14 days as `STALE`;
|
|
247
|
+
developers retire them with `/session-resume delete <id>` (no auto-deletion).
|
|
248
|
+
|
|
214
249
|
## Integrity
|
|
215
250
|
|
|
216
251
|
`node knowledge.js validate` runs after every `capture` write and enforces:
|
package/knowledge/INDEX.md
CHANGED
|
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
21
21
|
- **dbchanges2** (Database Changes) _(framework core)_ — 1 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
22
22
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
23
23
|
- **saml** (SAML SSO Gateway) — 2 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
24
|
-
- **toga2-view** (TOGa View Frontend) —
|
|
24
|
+
- **toga2-view** (TOGa View Frontend) — 1 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
|
|
25
25
|
- **toga2-hub** (TOGa Hub) — 2 doc(s) → [2.0/apps/toga2-hub/INDEX.md](2.0/apps/toga2-hub/INDEX.md)
|
|
26
26
|
- **talos** (TOGa IQ) — 6 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
|
|
27
27
|
- **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
|
|
@@ -4,4 +4,5 @@
|
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
5
|
| [Rate Monthly Reconciliation Report](features/monthly-reconciliation-report.md) | 1.0 | A monthly cron that emails an Excel reconciliation report covering all Rate subscription sales orders and their linked PayPal payments for the prior calendar mo | worker/crons/notifications/reports/rate/send_monthly_rate_purchases_report.php, worker/schedules/cron.worker.notification.json |
|
|
6
6
|
| [Rate SAML SSO](features/saml-sso.md) | 2.0 | Rate uses Azure AD as its IdP (`login.rate.com`). | _underscore/Model/Rate/ClientAuthentication.php, saml/Controller/Index.php, toga2-view/src/hooks/useAuthenticationFlow.ts |
|
|
7
|
+
| [Service Card Entitlement Display](features/service-card-entitlements.md) | 2.0 | Rate's home and services pages display one service card per purchased entitlement. | src/components/ServiceCard/ServiceCard.tsx, src/components/ServiceCard/index.ts, src/hooks/useBundleServices.ts, src/pages/Home/api/homeApi.ts, src/pages/Home/view/HomePage.tsx, src/pages/Home/viewModels/useHomePageViewModel.ts, src/pages/Services/view/ServicesPage.tsx, src/pages/Services/viewModels/useServicePageViewModel.ts |
|
|
7
8
|
| [Rate](profile.md) | 2.0 | Rate is a mortgage/lending client. | |
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Service Card Entitlement Display"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: toga2-view
|
|
5
|
+
project: TOGa View
|
|
6
|
+
client: rate
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-23
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- src/components/ServiceCard/ServiceCard.tsx
|
|
13
|
+
- src/components/ServiceCard/index.ts
|
|
14
|
+
- src/hooks/useBundleServices.ts
|
|
15
|
+
- src/pages/Home/api/homeApi.ts
|
|
16
|
+
- src/pages/Home/view/HomePage.tsx
|
|
17
|
+
- src/pages/Home/viewModels/useHomePageViewModel.ts
|
|
18
|
+
- src/pages/Services/view/ServicesPage.tsx
|
|
19
|
+
- src/pages/Services/viewModels/useServicePageViewModel.ts
|
|
20
|
+
related:
|
|
21
|
+
- clients/rate/profile.md
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Summary
|
|
25
|
+
|
|
26
|
+
Rate's home and services pages display one service card per purchased entitlement. The card
|
|
27
|
+
shows a hero image, title, Active badge, contract number, plan, price (or address for warranty
|
|
28
|
+
cards), renewal date, and a Manage button. Previously the UI showed one card per bundle type
|
|
29
|
+
regardless of how many entitlements the user had purchased.
|
|
30
|
+
|
|
31
|
+
## Key files / entry points
|
|
32
|
+
|
|
33
|
+
- `ServiceCard.tsx` — the card component; accepts `serviceType` to switch the second data row
|
|
34
|
+
between Price (tech) and Address (warranty)
|
|
35
|
+
- `useBundleServices.ts` — shared hook returning one `BundleService` per active entitlement;
|
|
36
|
+
used by both `useHomePageViewModel` and `useServicePageViewModel`
|
|
37
|
+
- `homeApi.ts` — `getActiveServices()` fetches entitlements with subscription and address fields
|
|
38
|
+
- `HomePage.tsx` — shows max 3 cards; "View All Services" tertiary text button links to `/services`
|
|
39
|
+
- `ServicesPage.tsx` — shows all entitlements in a 2-column grid, scrollable
|
|
40
|
+
|
|
41
|
+
## How it works
|
|
42
|
+
|
|
43
|
+
1. `getActiveServices(borrowerId)` queries `Entitlements` with:
|
|
44
|
+
- INNER JOIN: `Contacts`, `Customers`, `Subscriptions`
|
|
45
|
+
- LEFT JOIN (`ojoin`): `ContactAddresses`, `Addresses` — keeps entitlements whose contact
|
|
46
|
+
has no address instead of dropping the row
|
|
47
|
+
- Fields: `uuid`, `number`, `saleItem.title/uuid`, `subscription.isActive/amount/frequency/dateEnd`,
|
|
48
|
+
`contact.primaryContactAddress.address.line1`
|
|
49
|
+
|
|
50
|
+
2. `useBundleServices` filters `subscription.isActive === true` and builds one `BundleService`
|
|
51
|
+
per entitlement. It also fetches bundles to resolve `bundleUuid` for navigation.
|
|
52
|
+
|
|
53
|
+
3. `detectServiceType(title)` classifies each card:
|
|
54
|
+
- `'warranty'` — title includes "warranty" or "whole home" (and NOT "tech")
|
|
55
|
+
- `'tech'` — title includes "tech" or "support"
|
|
56
|
+
- `'other'` — everything else
|
|
57
|
+
|
|
58
|
+
4. `ServiceCard` renders the data rows based on `serviceType`:
|
|
59
|
+
- Tech / other → row 2: **Price** (`$XX.XX`)
|
|
60
|
+
- Warranty → row 2: **Address** (`contact.primaryContactAddress.address.line1`)
|
|
61
|
+
|
|
62
|
+
5. Home page slices `allServices` to 3 (`HOME_SERVICE_LIMIT`). If more exist, a tertiary
|
|
63
|
+
"View All Services" text link appears below the cards pointing to `/services`.
|
|
64
|
+
|
|
65
|
+
6. Services page renders all entitlements with no cap in a `grid-cols-1 md:grid-cols-2` grid.
|
|
66
|
+
|
|
67
|
+
## Data model
|
|
68
|
+
|
|
69
|
+
Address path in DB:
|
|
70
|
+
```
|
|
71
|
+
Entitlements.contactId
|
|
72
|
+
→ Contacts.primaryContactAddressId
|
|
73
|
+
→ ContactAddresses.id → ContactAddresses.addressId
|
|
74
|
+
→ Addresses.line1
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Contract number comes from `Entitlements.number` (auto-incremented with ET prefix, e.g. `ET100017`).
|
|
78
|
+
|
|
79
|
+
## Client variations
|
|
80
|
+
|
|
81
|
+
This feature is Rate-specific. The warranty card showing Address instead of Price is driven
|
|
82
|
+
by Rate's product mix (Whole Home Warranty + Tech Support). Other clients with different
|
|
83
|
+
product types would need `detectServiceType` extended or overridden.
|
|
84
|
+
|
|
85
|
+
## Gotchas / known issues
|
|
86
|
+
|
|
87
|
+
- **`ojoin` is required for address** — using INNER JOIN (`join`) drops every entitlement
|
|
88
|
+
whose contact has no `primaryContactAddressId`, silently hiding those cards.
|
|
89
|
+
- **`T00:00:00` in date formatting** — `formatDate` appends this before parsing to avoid
|
|
90
|
+
timezone offset shifting the renewal date back by one day.
|
|
91
|
+
- **Per-entitlement, not per-bundle** — `useBundleServices` intentionally does not dedup.
|
|
92
|
+
A user with 8 entitlements of the same product sees 8 cards. The old dedup-by-bundle
|
|
93
|
+
behavior (1 card per service type) was the previous design.
|
|
94
|
+
- **Inactive bundles are not shown** — the new design only renders active entitlements.
|
|
95
|
+
The "Add a subscription" dashed button (handled by `useAddSubscription` hook +
|
|
96
|
+
`AddSubscriptionSheet`) replaces the old "Get Started" inactive bundle cards.
|
|
97
|
+
- **`useBundleServices` is shared** — both `useHomePageViewModel` and `useServicePageViewModel`
|
|
98
|
+
call it. Changes to the hook affect both pages.
|
|
99
|
+
|
|
100
|
+
## Change history
|
|
101
|
+
|
|
102
|
+
- 2026-06-23 — Initial implementation: per-entitlement card display, new ServiceCard component,
|
|
103
|
+
address via LEFT JOIN, 3-card home limit with View All Services tertiary button (bala)
|
package/knowledge.js
CHANGED
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
* manifest
|
|
22
22
|
* kickoff-preflight --repos=a,b,c --client=<slug> --layer=<...> --q=<...>
|
|
23
23
|
* publish --msg= --mirror= --branch=
|
|
24
|
+
* session-save --file=<path> --msg= --branch=
|
|
25
|
+
* session-list --stale=<days> --mine[=<author>] --json --branch=
|
|
26
|
+
* session-show <id> --branch=
|
|
27
|
+
* session-delete <id> --msg= --branch=
|
|
28
|
+
* skills --installed=<.claude/skills dir> --json
|
|
24
29
|
*/
|
|
25
30
|
|
|
26
31
|
'use strict';
|
|
@@ -723,6 +728,221 @@ function cmdValidate() {
|
|
|
723
728
|
else console.log('validate: FAILED — see errors above');
|
|
724
729
|
}
|
|
725
730
|
|
|
731
|
+
/* ------------------------------------------------------------------ */
|
|
732
|
+
/* team-shared sessions — knowledge/sessions/<id>.md ride the same git */
|
|
733
|
+
/* rails as publish (rebase-before-push to _main). allDocs() does NOT */
|
|
734
|
+
/* walk sessions/, so validate/index ignore them; session-save runs its */
|
|
735
|
+
/* own lightweight frontmatter + secret check below. */
|
|
736
|
+
/* ------------------------------------------------------------------ */
|
|
737
|
+
|
|
738
|
+
const SESSIONS_DIR = path.join(ROOT, 'sessions');
|
|
739
|
+
const SESSION_REQUIRED = ['type', 'slug', 'title', 'author', 'status', 'created', 'updated'];
|
|
740
|
+
|
|
741
|
+
function gitRun(cmd, timeout) {
|
|
742
|
+
const opts = { cwd: __dirname, encoding: 'utf8', stdio: 'pipe' };
|
|
743
|
+
if (timeout) opts.timeout = timeout;
|
|
744
|
+
return require('child_process').execSync(cmd, opts);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function isGitClone() {
|
|
748
|
+
try { gitRun('git rev-parse --git-dir'); return true; } catch { return false; }
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function currentAuthor() {
|
|
752
|
+
try { return gitRun('git config user.name').trim(); } catch { return ''; }
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/* commit whatever is already staged, then rebase-before-push (3x retry */
|
|
756
|
+
/* for the CI version-bump race). Caller stages via git add / git rm. */
|
|
757
|
+
/* Returns a bare status token; callers map it to a SESSION:/SKILLS: line.*/
|
|
758
|
+
function pushStaged(opts) {
|
|
759
|
+
const branch = (opts && opts.branch) || '_main';
|
|
760
|
+
const restrict = opts && opts.restrictPrefix;
|
|
761
|
+
const staged = gitRun('git diff --cached --name-only').split('\n').map(s => s.trim()).filter(Boolean);
|
|
762
|
+
if (!staged.length) return 'NO_CHANGES';
|
|
763
|
+
if (restrict) {
|
|
764
|
+
const outside = staged.filter(f => !f.startsWith(restrict));
|
|
765
|
+
if (outside.length) { gitRun('git reset -q'); return 'ABORT_OUTSIDE:' + outside.join(', '); }
|
|
766
|
+
}
|
|
767
|
+
const msg = String((opts && opts.msg) || 'sessions: update').replace(/["\\]/g, "'").slice(0, 200);
|
|
768
|
+
gitRun(`git commit -m "${msg}"`);
|
|
769
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
770
|
+
try {
|
|
771
|
+
gitRun('git fetch origin');
|
|
772
|
+
gitRun(`git rebase origin/${branch}`);
|
|
773
|
+
gitRun(`git push origin HEAD:${branch}`);
|
|
774
|
+
return 'PUSHED';
|
|
775
|
+
} catch (e) {
|
|
776
|
+
let status = '';
|
|
777
|
+
try { status = gitRun('git status --porcelain'); } catch { /* ignore */ }
|
|
778
|
+
if (/^(UU|AA|DD|AU|UA|DU|UD) /m.test(status)) {
|
|
779
|
+
try { gitRun('git rebase --abort'); } catch { /* ignore */ }
|
|
780
|
+
return 'CONFLICT';
|
|
781
|
+
}
|
|
782
|
+
// else: almost certainly a fresh CI bump (non-fast-forward) — retry
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return 'PUSH_FAILED';
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function reportSession(status, branch) {
|
|
789
|
+
const br = branch || '_main';
|
|
790
|
+
if (status === 'PUSHED') { console.log(`SESSION: PUSHED to ${br} — teammates see it on the next /session-resume (git fetch)`); return; }
|
|
791
|
+
if (status === 'DELETED') { console.log(`SESSION: DELETED from ${br}`); return; }
|
|
792
|
+
if (status === 'NO_CHANGES') { console.log('SESSION: NO_CHANGES — nothing new to push'); return; }
|
|
793
|
+
if (status === 'CONFLICT') { console.log(`SESSION: CONFLICT — rebase onto origin/${br} hit a real conflict; resolve manually, then retry`); process.exitCode = 1; return; }
|
|
794
|
+
if (status && status.startsWith('ABORT_OUTSIDE')) { console.log('SESSION: ABORT_OUTSIDE_SESSIONS — refusing to commit non-session files: ' + status.slice('ABORT_OUTSIDE:'.length)); process.exitCode = 1; return; }
|
|
795
|
+
console.log(`SESSION: PUSH_FAILED after 3 attempts — run manually: git -C "${__dirname}" push origin HEAD:${br}`); process.exitCode = 1;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function fetchQuiet() {
|
|
799
|
+
try { gitRun('git fetch origin', 6000); return true; } catch { return false; }
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/* List session files from origin/<branch> WITHOUT touching the working */
|
|
803
|
+
/* tree (so a teammate's just-pushed session shows up after a fetch even */
|
|
804
|
+
/* if it isn't checked out locally). Falls back to the working tree. */
|
|
805
|
+
function listSessionDocs(branch, doFetch) {
|
|
806
|
+
const br = branch || '_main';
|
|
807
|
+
if (isGitClone()) {
|
|
808
|
+
if (doFetch) fetchQuiet();
|
|
809
|
+
let names = null;
|
|
810
|
+
try { names = gitRun(`git ls-tree -r --name-only origin/${br} knowledge/sessions/`).split('\n').map(s => s.trim()).filter(f => f.endsWith('.md')); }
|
|
811
|
+
catch { names = null; }
|
|
812
|
+
if (names && names.length) {
|
|
813
|
+
const out = [];
|
|
814
|
+
for (const rel of names) {
|
|
815
|
+
try { out.push({ id: path.basename(rel, '.md'), rel, content: gitRun(`git show origin/${br}:${rel}`) }); } catch { /* skip */ }
|
|
816
|
+
}
|
|
817
|
+
if (out.length) return out;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
// fallback: local working tree
|
|
821
|
+
if (!fs.existsSync(SESSIONS_DIR)) return [];
|
|
822
|
+
return fs.readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.md')).map(f => ({
|
|
823
|
+
id: path.basename(f, '.md'), rel: 'knowledge/sessions/' + f, content: fs.readFileSync(path.join(SESSIONS_DIR, f), 'utf8'),
|
|
824
|
+
}));
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function ageDays(dateStr) {
|
|
828
|
+
const m = String(dateStr || '').match(/(\d{4})-(\d{2})-(\d{2})/);
|
|
829
|
+
if (!m) return null;
|
|
830
|
+
const d = Date.UTC(+m[1], +m[2] - 1, +m[3]);
|
|
831
|
+
const now = new Date();
|
|
832
|
+
const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
833
|
+
return Math.round((today - d) / 86400000);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function padTo(s, n) { s = String(s); return s.length >= n ? s : s + ' '.repeat(n - s.length); }
|
|
837
|
+
function clip(s, n) { s = String(s).replace(/\s+/g, ' ').trim(); return s.length > n ? s.slice(0, n - 1) + '…' : s; }
|
|
838
|
+
|
|
839
|
+
function cmdSessionSave(args) {
|
|
840
|
+
const file = args.file;
|
|
841
|
+
if (!file) return fail('session-save requires --file=<path>');
|
|
842
|
+
const abs = path.resolve(String(file));
|
|
843
|
+
if (abs !== path.join(SESSIONS_DIR, path.basename(abs)) && !abs.startsWith(SESSIONS_DIR + path.sep)) {
|
|
844
|
+
console.log('SESSION: ABORT_OUTSIDE_SESSIONS — file must live under knowledge/sessions/'); process.exitCode = 1; return;
|
|
845
|
+
}
|
|
846
|
+
if (!fs.existsSync(abs)) return fail('session file not found: ' + abs);
|
|
847
|
+
const content = fs.readFileSync(abs, 'utf8');
|
|
848
|
+
const { data } = parseFrontmatter(content);
|
|
849
|
+
const missing = SESSION_REQUIRED.filter(k => !data[k]);
|
|
850
|
+
if (missing.length) { console.log('SESSION: INVALID — missing frontmatter: ' + missing.join(', ')); process.exitCode = 1; return; }
|
|
851
|
+
if (data.type !== 'session') { console.log('SESSION: INVALID — type must be "session"'); process.exitCode = 1; return; }
|
|
852
|
+
if (!['active', 'stale'].includes(data.status)) { console.log('SESSION: INVALID — status must be active|stale'); process.exitCode = 1; return; }
|
|
853
|
+
const secrets = scanSecrets(content);
|
|
854
|
+
if (secrets.length) { console.log('SESSION: SECRET — ' + secrets.map(s => `line ${s.line}: ${s.reason}`).join('; ') + '; redact before saving'); process.exitCode = 1; return; }
|
|
855
|
+
if (!isGitClone()) { console.log('SESSION: NOT_GIT — session written locally only; run `npx toga-ai` to create a pushable clone'); return; }
|
|
856
|
+
const rel = 'knowledge/sessions/' + path.basename(abs);
|
|
857
|
+
gitRun(`git add "${rel}"`);
|
|
858
|
+
reportSession(pushStaged({ msg: args.msg || ('session: ' + data.slug), branch: args.branch, restrictPrefix: 'knowledge/sessions/' }), args.branch);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function cmdSessionList(args) {
|
|
862
|
+
const rows = listSessionDocs(args.branch, true).map(e => {
|
|
863
|
+
const { data } = parseFrontmatter(e.content);
|
|
864
|
+
const updated = data.updated || data.created || '';
|
|
865
|
+
const age = ageDays(updated);
|
|
866
|
+
return {
|
|
867
|
+
id: e.id, date: updated, author: data.author || '', framework: data.framework || '',
|
|
868
|
+
repos: Array.isArray(data.repos) ? data.repos.join(' ') : (data.repos || ''),
|
|
869
|
+
client: data.client || '', title: data.title || '', age,
|
|
870
|
+
};
|
|
871
|
+
});
|
|
872
|
+
const staleDays = args.stale !== undefined && args.stale !== true ? parseInt(args.stale, 10) : 14;
|
|
873
|
+
const mine = args.mine ? (typeof args.mine === 'string' ? args.mine : currentAuthor()) : null;
|
|
874
|
+
const enriched = rows
|
|
875
|
+
.map(r => ({ ...r, stale: (Number.isInteger(staleDays) && r.age !== null && r.age > staleDays) }))
|
|
876
|
+
.filter(r => !mine || r.author === mine)
|
|
877
|
+
.sort((a, b) => String(b.date).localeCompare(String(a.date)));
|
|
878
|
+
if (args.json) { console.log(JSON.stringify(enriched)); return; }
|
|
879
|
+
if (!enriched.length) { console.log('No team sessions saved yet.'); return; }
|
|
880
|
+
console.log(padTo('', 6) + ' ' + padTo('DATE', 10) + ' ' + padTo('AUTHOR', 12) + ' ' + padTo('AGE', 5) + ' ' + padTo('ID', 40) + ' TASK');
|
|
881
|
+
for (const r of enriched) {
|
|
882
|
+
console.log(
|
|
883
|
+
padTo(r.stale ? 'STALE' : 'ok', 6) + ' ' + padTo(r.date, 10) + ' ' + padTo(clip(r.author, 12), 12) + ' ' +
|
|
884
|
+
padTo((r.age === null ? '?' : r.age + 'd'), 5) + ' ' + padTo(clip(r.id, 40), 40) + ' ' + clip(r.title, 60)
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function cmdSessionShow(args) {
|
|
890
|
+
const id = args.id || (args._ && args._[0]);
|
|
891
|
+
if (!id) return fail('session-show requires an id (filename without .md)');
|
|
892
|
+
const match = listSessionDocs(args.branch, true).find(e => e.id === id || e.id.includes(String(id)));
|
|
893
|
+
if (!match) { console.log('SESSION: NOT_FOUND — no session matching "' + id + '" (try: session-list)'); process.exitCode = 1; return; }
|
|
894
|
+
process.stdout.write(match.content);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function cmdSessionDelete(args) {
|
|
898
|
+
const id = args.id || (args._ && args._[0]);
|
|
899
|
+
if (!id) return fail('session-delete requires an id');
|
|
900
|
+
if (!isGitClone()) { console.log('SESSION: NOT_GIT — cannot delete a team session without a git clone'); process.exitCode = 1; return; }
|
|
901
|
+
const br = args.branch || '_main';
|
|
902
|
+
fetchQuiet();
|
|
903
|
+
const rel = 'knowledge/sessions/' + id + '.md';
|
|
904
|
+
// make sure the file is present in the working tree so git rm can stage its removal
|
|
905
|
+
try { gitRun(`git checkout origin/${br} -- "${rel}"`); } catch { /* may already be local or absent */ }
|
|
906
|
+
if (!fs.existsSync(path.join(__dirname, rel))) { console.log('SESSION: NOT_FOUND — no session "' + id + '" on origin/' + br); process.exitCode = 1; return; }
|
|
907
|
+
gitRun(`git rm -q "${rel}"`);
|
|
908
|
+
const st = pushStaged({ msg: args.msg || ('session: delete ' + id), branch: br, restrictPrefix: 'knowledge/sessions/' });
|
|
909
|
+
reportSession(st === 'PUSHED' ? 'DELETED' : st, br);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/* ------------------------------------------------------------------ */
|
|
913
|
+
/* command: skills — generated, always-current listing of team skills */
|
|
914
|
+
/* (the README table drifts; skills frontmatter is the source of truth).*/
|
|
915
|
+
/* --installed=<.claude/skills dir> marks installed/available/local-only.*/
|
|
916
|
+
/* ------------------------------------------------------------------ */
|
|
917
|
+
|
|
918
|
+
function cmdSkills(args) {
|
|
919
|
+
const skillsSrc = path.join(__dirname, 'skills');
|
|
920
|
+
const teamSkills = fs.existsSync(skillsSrc)
|
|
921
|
+
? fs.readdirSync(skillsSrc, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name) : [];
|
|
922
|
+
const meta = {};
|
|
923
|
+
for (const s of teamSkills) {
|
|
924
|
+
const skf = path.join(skillsSrc, s, 'SKILL.md');
|
|
925
|
+
let name = s, desc = '';
|
|
926
|
+
if (fs.existsSync(skf)) { const { data } = parseFrontmatter(fs.readFileSync(skf, 'utf8')); name = data.name || s; desc = data.description || ''; }
|
|
927
|
+
meta[s] = { name, desc };
|
|
928
|
+
}
|
|
929
|
+
let installed = null;
|
|
930
|
+
if (args.installed && typeof args.installed === 'string') {
|
|
931
|
+
const dir = path.resolve(args.installed);
|
|
932
|
+
installed = fs.existsSync(dir) ? fs.readdirSync(dir, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name) : [];
|
|
933
|
+
}
|
|
934
|
+
const rows = teamSkills.map(s => ({
|
|
935
|
+
skill: s, name: meta[s].name, description: meta[s].desc,
|
|
936
|
+
status: installed ? (installed.includes(s) ? 'installed' : 'available') : 'team',
|
|
937
|
+
}));
|
|
938
|
+
if (installed) for (const s of installed) if (!teamSkills.includes(s)) rows.push({ skill: s, name: s, description: '(local only — not in team repo)', status: 'local-only' });
|
|
939
|
+
rows.sort((a, b) => a.skill.localeCompare(b.skill));
|
|
940
|
+
if (args.json) { console.log(JSON.stringify(rows)); return; }
|
|
941
|
+
console.log(padTo('STATUS', 11) + ' ' + padTo('SKILL', 22) + ' DESCRIPTION');
|
|
942
|
+
for (const r of rows) console.log(padTo(r.status, 11) + ' ' + padTo(clip(r.skill, 22), 22) + ' ' + clip(r.description, 90));
|
|
943
|
+
if (installed && rows.some(r => r.status === 'available')) console.log('\nRun /sync-team-skills to install the "available" skills.');
|
|
944
|
+
}
|
|
945
|
+
|
|
726
946
|
/* ------------------------------------------------------------------ */
|
|
727
947
|
/* dispatch */
|
|
728
948
|
/* ------------------------------------------------------------------ */
|
|
@@ -730,6 +950,7 @@ function cmdValidate() {
|
|
|
730
950
|
function main() {
|
|
731
951
|
const [, , cmd, ...rest] = process.argv;
|
|
732
952
|
const args = parseArgs(rest);
|
|
953
|
+
args._ = rest.filter(a => !a.startsWith('--'));
|
|
733
954
|
switch (cmd) {
|
|
734
955
|
case 'search': return cmdSearch(args);
|
|
735
956
|
case 'index': return cmdIndex();
|
|
@@ -738,8 +959,13 @@ function main() {
|
|
|
738
959
|
case 'manifest': return cmdManifest();
|
|
739
960
|
case 'kickoff-preflight': return cmdPreflight(args);
|
|
740
961
|
case 'publish': return cmdPublish(args);
|
|
962
|
+
case 'session-save': return cmdSessionSave(args);
|
|
963
|
+
case 'session-list': return cmdSessionList(args);
|
|
964
|
+
case 'session-show': return cmdSessionShow(args);
|
|
965
|
+
case 'session-delete': return cmdSessionDelete(args);
|
|
966
|
+
case 'skills': return cmdSkills(args);
|
|
741
967
|
default:
|
|
742
|
-
console.log('Usage: node knowledge.js <search|index|deps|validate|manifest|kickoff-preflight|publish> [--flags]');
|
|
968
|
+
console.log('Usage: node knowledge.js <search|index|deps|validate|manifest|kickoff-preflight|publish|session-save|session-list|session-show|session-delete|skills> [--flags]');
|
|
743
969
|
process.exitCode = 1;
|
|
744
970
|
}
|
|
745
971
|
}
|
package/package.json
CHANGED
|
@@ -24,6 +24,32 @@ function checkUnfinishedSession(cwd) {
|
|
|
24
24
|
return fs.existsSync(sessionFile);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Resolve the team `claude` repo (same order kickoff uses), or null.
|
|
28
|
+
function resolveTeamRepo() {
|
|
29
|
+
const candidates = [];
|
|
30
|
+
if (process.env.CLAUDE_TEAM_REPO) candidates.push(process.env.CLAUDE_TEAM_REPO);
|
|
31
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
32
|
+
if (home) candidates.push(path.join(home, 'toga-tech'), path.join(home, 'claude'));
|
|
33
|
+
for (const dir of candidates) {
|
|
34
|
+
try { if (dir && fs.existsSync(path.join(dir, 'knowledge', 'registry.json'))) return dir; } catch { /* ignore */ }
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Best-effort, time-boxed peek at team sessions. Never throws; returns [] on any
|
|
40
|
+
// failure (offline, no clone, slow fetch) so startup stays fast and quiet.
|
|
41
|
+
function teamSessions(teamRepo) {
|
|
42
|
+
if (!teamRepo) return [];
|
|
43
|
+
try {
|
|
44
|
+
const out = require('child_process').execSync(
|
|
45
|
+
'node "' + path.join(teamRepo, 'knowledge.js') + '" session-list --json',
|
|
46
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3500 }
|
|
47
|
+
);
|
|
48
|
+
const rows = JSON.parse(out);
|
|
49
|
+
return Array.isArray(rows) ? rows : [];
|
|
50
|
+
} catch { return []; }
|
|
51
|
+
}
|
|
52
|
+
|
|
27
53
|
function main() {
|
|
28
54
|
const cwd = process.cwd();
|
|
29
55
|
const now = new Date().toLocaleDateString('en-US', {
|
|
@@ -48,6 +74,18 @@ function main() {
|
|
|
48
74
|
console.log('');
|
|
49
75
|
}
|
|
50
76
|
|
|
77
|
+
// Surface team-shared sessions (best-effort, time-boxed — never blocks startup).
|
|
78
|
+
const teamRepo = resolveTeamRepo();
|
|
79
|
+
const sessions = teamSessions(teamRepo);
|
|
80
|
+
if (sessions.length) {
|
|
81
|
+
const newest = sessions[0];
|
|
82
|
+
const fresh = sessions.filter(s => !s.stale).length;
|
|
83
|
+
console.log(`${sessions.length} team session(s) available${fresh < sessions.length ? ` (${sessions.length - fresh} stale)` : ''}.`);
|
|
84
|
+
console.log(` Newest: ${newest.id} by ${newest.author || 'unknown'} — "${newest.title || ''}"`);
|
|
85
|
+
console.log(' Run /session-resume <id> to continue one, or /session-resume list to see all.');
|
|
86
|
+
console.log('');
|
|
87
|
+
}
|
|
88
|
+
|
|
51
89
|
console.log('Ready. Run /kickoff to prime context for your work session.');
|
|
52
90
|
console.log('');
|
|
53
91
|
|
|
@@ -9,55 +9,70 @@ You are restoring context from a prior session so that work can continue without
|
|
|
9
9
|
information loss. The briefing you provide must be actionable and explicit about what
|
|
10
10
|
NOT to do — failed approaches from prior sessions are as important as what worked.
|
|
11
11
|
|
|
12
|
+
Sessions are **shared across the team**: they live in the team `claude` repo under
|
|
13
|
+
`knowledge/sessions/` and are pulled fresh on every resume, so you can pick up a session a
|
|
14
|
+
teammate saved (not just your own).
|
|
15
|
+
|
|
12
16
|
## Arguments
|
|
13
17
|
|
|
14
18
|
```
|
|
15
|
-
/session-resume [
|
|
19
|
+
/session-resume [latest | <slug-or-id> | list | delete <id>]
|
|
16
20
|
```
|
|
17
21
|
|
|
18
|
-
- `
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
- `latest` — load the most recently updated team session.
|
|
23
|
+
- `<slug-or-id>` — load by slug (e.g. `auth-refactor`) or full id
|
|
24
|
+
(`2026-06-08-auth-refactor-asmith`).
|
|
25
|
+
- `list` — show all team sessions (with author + age + stale flags) and stop.
|
|
26
|
+
- `delete <id>` — remove a stale team session (confirm first), then stop.
|
|
27
|
+
- If omitted, list and ask which to load.
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
First resolve `TEAM_REPO` exactly as `kickoff`/`session-save` do (the `team-repo-path`
|
|
30
|
+
memory, else `$CLAUDE_TEAM_REPO`, else probe `~/toga-tech` … for `knowledge/registry.json`).
|
|
31
|
+
All commands below run `node "<TEAM_REPO>/knowledge.js" …`, which **git-fetches first** so
|
|
32
|
+
teammates' just-pushed sessions are visible.
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
---
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
No saved sessions found in ~/.claude/session-data/
|
|
36
|
+
## Step 1 — Discover available sessions (always fetches latest)
|
|
31
37
|
|
|
32
|
-
|
|
38
|
+
```bash
|
|
39
|
+
node "<TEAM_REPO>/knowledge.js" session-list
|
|
33
40
|
```
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
This prints a table (`STATUS DATE AUTHOR AGE ID TASK`), newest first, flagging sessions
|
|
43
|
+
older than 14 days as `STALE`. Add `--mine=<author>` to filter to your own, `--stale=<days>`
|
|
44
|
+
to change the threshold. If it prints "No team sessions saved yet.", say so and stop:
|
|
38
45
|
|
|
39
46
|
```
|
|
40
|
-
|
|
41
|
-
1. 2026-06-08 — auth-refactor ~/.claude/session-data/2026-06-08-auth-refactor-session.md
|
|
42
|
-
2. 2026-06-07 — worker-cron-fix ~/.claude/session-data/2026-06-07-worker-cron-fix-session.md
|
|
43
|
-
3. 2026-06-05 — api2-pagination ~/.claude/session-data/2026-06-05-api2-pagination-session.md
|
|
47
|
+
No team sessions saved yet. Save one at the end of your work: /session-save [name]
|
|
44
48
|
```
|
|
45
49
|
|
|
50
|
+
**`list` mode:** render this table and stop here.
|
|
51
|
+
|
|
52
|
+
**`delete <id>` mode:** confirm with the developer, then run
|
|
53
|
+
`node "<TEAM_REPO>/knowledge.js" session-delete <id> --msg="session: retire <id>"`,
|
|
54
|
+
report the `SESSION: DELETED|NOT_FOUND|CONFLICT` result, and stop.
|
|
55
|
+
|
|
46
56
|
---
|
|
47
57
|
|
|
48
58
|
## Step 2 — Resolve which session to load
|
|
49
59
|
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
-
|
|
60
|
+
- `latest` → the top row (newest `updated`).
|
|
61
|
+
- `<slug-or-id>` → the row whose id equals it or contains the slug. If several match
|
|
62
|
+
(same slug, different authors/dates), show those candidates (date + author) and ask which.
|
|
63
|
+
- If none match: "No session found matching '<x>'. Available: [list ids]."
|
|
64
|
+
- No argument → ask: "Which session should I load? (id or slug)"
|
|
55
65
|
|
|
56
66
|
---
|
|
57
67
|
|
|
58
68
|
## Step 3 — Load and parse the session file
|
|
59
69
|
|
|
60
|
-
|
|
70
|
+
```bash
|
|
71
|
+
node "<TEAM_REPO>/knowledge.js" session-show <id>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This prints the full session file (frontmatter + body) from the freshest team state.
|
|
75
|
+
Parse these sections from the body:
|
|
61
76
|
- **Task** — the one-sentence summary
|
|
62
77
|
- **Project/Repo** — repo and framework
|
|
63
78
|
- **Date** — when it was saved
|
|
@@ -77,8 +92,8 @@ Output a structured briefing in this exact format:
|
|
|
77
92
|
|
|
78
93
|
```
|
|
79
94
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
80
|
-
Session Resume: <
|
|
81
|
-
Saved
|
|
95
|
+
Session Resume: <slug>
|
|
96
|
+
Saved by <author> on <YYYY-MM-DD> | Repo: <repo> (<framework>)
|
|
82
97
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
83
98
|
|
|
84
99
|
TASK
|
|
@@ -153,4 +168,8 @@ model accordingly.
|
|
|
153
168
|
- If the "Exact next step" section is blank or unclear, ask the developer what they
|
|
154
169
|
want to tackle before proceeding.
|
|
155
170
|
- Recommend running `/session-save` again at the end of this session to update the
|
|
156
|
-
record with new progress.
|
|
171
|
+
team record with new progress (re-saving the same slug overwrites and re-pushes it).
|
|
172
|
+
- You may be resuming a **teammate's** session — the briefing header names the author.
|
|
173
|
+
When you re-save, it saves under *your* author id, leaving theirs intact.
|
|
174
|
+
- Stale sessions accumulate: when `session-list` flags old ones you recognize as done,
|
|
175
|
+
retire them with `/session-resume delete <id>`.
|
|
@@ -67,30 +67,53 @@ when they resume — no thinking required.
|
|
|
67
67
|
|
|
68
68
|
---
|
|
69
69
|
|
|
70
|
-
## Step 2 —
|
|
70
|
+
## Step 2 — Resolve the team repo, author, and save path
|
|
71
|
+
|
|
72
|
+
Sessions are **shared with the whole team by default** — saved into the team `claude`
|
|
73
|
+
repo and pushed, so any teammate can resume them (mirrors how `capture` publishes).
|
|
74
|
+
|
|
75
|
+
1. **Team repo** — resolve `TEAM_REPO` the same way `kickoff` does: the `team-repo-path`
|
|
76
|
+
Claude memory first, else `$CLAUDE_TEAM_REPO`, else auto-probe (`~/toga-tech`, …) for a
|
|
77
|
+
dir containing `knowledge/registry.json`. Persist a `team-repo-path` memory if you had
|
|
78
|
+
to discover it.
|
|
79
|
+
2. **Author** — the `author-username` Claude memory (the developer's KB username, e.g.
|
|
80
|
+
`jcardinal`). If absent, fall back to `git config user.name`.
|
|
81
|
+
3. **Slug** — the Step 0 name (or a derived kebab-case slug, max 4 words).
|
|
71
82
|
|
|
72
83
|
```
|
|
73
|
-
|
|
74
|
-
Also: <project-root>/.session-latest.md (gitignored, for quick local reference)
|
|
84
|
+
Team file: <TEAM_REPO>/knowledge/sessions/YYYY-MM-DD-<slug>-<author>.md
|
|
75
85
|
```
|
|
76
86
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
- `<name>` = the session name from Step 0
|
|
80
|
-
- `<project-root>` = the root of the project being worked on (use the `team-repo-path`
|
|
81
|
-
memory or the current working directory; do not guess if unknown)
|
|
87
|
+
The `<author>` suffix lets two developers use the same slug without collision.
|
|
88
|
+
`YYYY-MM-DD` is today's date. Create `<TEAM_REPO>/knowledge/sessions/` if it does not exist.
|
|
82
89
|
|
|
83
|
-
|
|
90
|
+
**No git clone / TEAM_REPO unresolved →** fall back to the legacy local-only path
|
|
91
|
+
`~/.claude/session-data/YYYY-MM-DD-<slug>-session.md` (create the dir if needed), warn the
|
|
92
|
+
developer that the session was saved **locally only** (no team push possible without a
|
|
93
|
+
clone — `npx toga-ai` creates one), and skip Step 4.
|
|
84
94
|
|
|
85
95
|
---
|
|
86
96
|
|
|
87
|
-
## Step 3 — Write the session file
|
|
97
|
+
## Step 3 — Write the session file (frontmatter + body)
|
|
88
98
|
|
|
89
|
-
Write the file
|
|
90
|
-
|
|
99
|
+
Write the team file using this exact template — **frontmatter first**, then the unchanged
|
|
100
|
+
9-section body. Do not reformat, summarize, or condense.
|
|
91
101
|
|
|
92
102
|
```markdown
|
|
93
|
-
|
|
103
|
+
---
|
|
104
|
+
type: session
|
|
105
|
+
slug: <slug>
|
|
106
|
+
title: <short human title from 1a>
|
|
107
|
+
author: <author>
|
|
108
|
+
repos: [<repo>, <repo>] # repos touched this session
|
|
109
|
+
framework: "1.0" | "2.0" | "both" | "standalone"
|
|
110
|
+
client: <client-slug> | shared
|
|
111
|
+
status: active # set to "stale" later if abandoned
|
|
112
|
+
created: YYYY-MM-DD
|
|
113
|
+
updated: YYYY-MM-DD
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
# Session: <slug>
|
|
94
117
|
**Date:** YYYY-MM-DD
|
|
95
118
|
**Project/Repo:** <repo> (<framework>)
|
|
96
119
|
**Task:** <one-sentence summary from 1a>
|
|
@@ -126,17 +149,39 @@ or condense — write the full content at both locations.
|
|
|
126
149
|
_Saved by /session-save on YYYY-MM-DD_
|
|
127
150
|
```
|
|
128
151
|
|
|
152
|
+
> **Never paste credentials into the body** (tokens, keys, passwords). The publisher
|
|
153
|
+
> secret-scans the file and refuses to push if it finds one. Reference the config/constant
|
|
154
|
+
> location instead.
|
|
155
|
+
|
|
129
156
|
---
|
|
130
157
|
|
|
131
|
-
## Step 4 —
|
|
158
|
+
## Step 4 — Push to the team (skip if Step 2 fell back to local-only)
|
|
159
|
+
|
|
160
|
+
Run the deterministic publisher — it validates the session frontmatter, secret-scans, and
|
|
161
|
+
does the rebase-before-push to `_main` (3× retry for the CI version-bump race), staging
|
|
162
|
+
**only** the session file:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
node "<TEAM_REPO>/knowledge.js" session-save --file="<the team file path>" --msg="session: <slug> — <task>"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Read the single `SESSION:` line and act:
|
|
169
|
+
- `PUSHED to _main` — done.
|
|
170
|
+
- `NO_CHANGES` — file identical to what's already pushed; nothing to do.
|
|
171
|
+
- `INVALID …` / `SECRET …` — fix the frontmatter / redact the value, then re-run.
|
|
172
|
+
- `CONFLICT` — a real merge conflict; resolve manually in `<TEAM_REPO>`, then re-run.
|
|
173
|
+
- `NOT_GIT` — no clone; the file is saved locally only (treat as the Step 2 fallback).
|
|
174
|
+
- `PUSH_FAILED` — after 3 retries; run the manual push command it prints.
|
|
175
|
+
|
|
176
|
+
---
|
|
132
177
|
|
|
133
|
-
|
|
178
|
+
## Step 5 — Confirm to developer
|
|
134
179
|
|
|
135
180
|
```
|
|
136
|
-
✓ Session saved:
|
|
137
|
-
✓ Latest copy: <project-root>/.session-latest.md
|
|
181
|
+
✓ Session saved to team: knowledge/sessions/YYYY-MM-DD-<slug>-<author>.md (pushed to _main)
|
|
138
182
|
|
|
139
|
-
To resume: /session-resume <
|
|
183
|
+
To resume (from any machine): /session-resume <slug> (or: /session-resume latest)
|
|
184
|
+
See all team sessions: /session-resume list
|
|
140
185
|
|
|
141
186
|
Session captured:
|
|
142
187
|
• What worked: <N items>
|
|
@@ -144,15 +189,16 @@ Session captured:
|
|
|
144
189
|
• Next step: <exact next step summary>
|
|
145
190
|
```
|
|
146
191
|
|
|
147
|
-
If
|
|
192
|
+
If the save fell back to local-only, say so plainly and give the local path instead.
|
|
148
193
|
|
|
149
194
|
---
|
|
150
195
|
|
|
151
196
|
## Notes
|
|
152
197
|
|
|
153
|
-
-
|
|
154
|
-
|
|
155
|
-
-
|
|
156
|
-
|
|
157
|
-
-
|
|
158
|
-
|
|
198
|
+
- **Team-by-default.** The whole point is cross-developer handoff: one dev saves, another
|
|
199
|
+
resumes. Local-only is just the offline/no-clone fallback.
|
|
200
|
+
- Do NOT compress or summarize the "What did NOT work" section. Future sessions depend on
|
|
201
|
+
exact failure reasons to avoid repeating dead ends.
|
|
202
|
+
- The session file lives under `knowledge/sessions/`, which `knowledge.js validate`/`index`
|
|
203
|
+
deliberately ignore — sessions are not knowledge docs and have their own listing
|
|
204
|
+
(`/session-resume list`).
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: team-skills
|
|
3
|
+
description: Lists every skill available in the team `claude` knowledge repo with its description, and marks which are installed in this project, available to pull, or local-only. Use when the developer says "what skills are available", "list team skills", "team skills", "show skills", "what can you do", or asks which team skills exist. Pairs with /sync-team-skills, which installs them.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Team Skills — discover what the team knowledge base offers
|
|
7
|
+
|
|
8
|
+
`/sync-team-skills` *installs* skills but there's no way to *see* what exists first. This
|
|
9
|
+
skill renders an always-current listing straight from each skill's `SKILL.md` frontmatter
|
|
10
|
+
(the README skill table is hand-maintained and drifts — do not use it as the source).
|
|
11
|
+
|
|
12
|
+
## Step 1 — Resolve the team repo
|
|
13
|
+
|
|
14
|
+
Resolve `TEAM_REPO` exactly as `kickoff` does: the `team-repo-path` Claude memory first,
|
|
15
|
+
else `$CLAUDE_TEAM_REPO`, else probe (`~/toga-tech`, …) for a dir containing
|
|
16
|
+
`knowledge/registry.json`. Persist a `team-repo-path` memory if you had to discover it.
|
|
17
|
+
|
|
18
|
+
Optionally `git -C "<TEAM_REPO>" pull --rebase --autostash origin _main` first so the list
|
|
19
|
+
reflects skills teammates just pushed. Skip the pull silently if it is not a clone or
|
|
20
|
+
network is unavailable.
|
|
21
|
+
|
|
22
|
+
## Step 2 — List skills with install status
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
node "<TEAM_REPO>/knowledge.js" skills --installed="<project-root>/.claude/skills"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`<project-root>` is the current working directory's project root (the dir containing
|
|
29
|
+
`.claude`). This prints a table `STATUS SKILL DESCRIPTION` where STATUS is one of:
|
|
30
|
+
|
|
31
|
+
- **installed** — present in this project's `.claude/skills/`; ready to use now.
|
|
32
|
+
- **available** — in the team repo but not installed here → run `/sync-team-skills`.
|
|
33
|
+
- **local-only** — installed locally but absent from the team repo (a personal/unpushed
|
|
34
|
+
skill, e.g. `worker2-action`). Flag it so the developer can decide whether to contribute it.
|
|
35
|
+
|
|
36
|
+
## Step 3 — Present and advise
|
|
37
|
+
|
|
38
|
+
Render the table grouped by status (installed first, then available, then local-only). Then:
|
|
39
|
+
|
|
40
|
+
- If any row is **available**, tell the developer: "Run `/sync-team-skills` to install these."
|
|
41
|
+
- If any row is **local-only**, note it: "`<skill>` exists only on this machine — if it's
|
|
42
|
+
useful to the team, add it to `<TEAM_REPO>/skills/` and push."
|
|
43
|
+
- Keep descriptions to one line each; the full skill prose lives in its `SKILL.md`.
|
|
44
|
+
|
|
45
|
+
## Notes
|
|
46
|
+
|
|
47
|
+
- This is read-only discovery. It never installs or modifies skills — that's
|
|
48
|
+
`/sync-team-skills`'s job.
|
|
49
|
+
- The canonical skill source is `<TEAM_REPO>/skills/*/SKILL.md` (what `sync-skills.js`
|
|
50
|
+
copies from), not the repo's `.claude/skills/` (an untracked local install).
|