kimal3 0.6.5 → 0.8.0
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/README.md +10 -4
- package/claude-plugins/.claude-plugin/marketplace.json +2 -1
- package/claude-plugins/README.md +2 -0
- package/claude-plugins/kimal3-logo/.claude-plugin/plugin.json +1 -1
- package/claude-plugins/kimal3-logo/hooks/app-oeffnen.ts +37 -9
- package/claude-plugins/kimal3-logo/hooks/register.tsx +1 -1
- package/claude-plugins/kimal3-plan/.claude-plugin/plugin.json +7 -0
- package/claude-plugins/kimal3-plan/hooks/erinnerung.ts +83 -0
- package/claude-plugins/kimal3-plan/hooks/fehlerzeile.ts +41 -0
- package/claude-plugins/kimal3-plan/hooks/hooks.json +1 -0
- package/claude-plugins/kimal3-plan/hooks/lauf.ts +143 -0
- package/claude-plugins/kimal3-plan/hooks/lebenszeichen.ts +197 -0
- package/claude-plugins/kimal3-plan/hooks/ordner.ts +141 -0
- package/claude-plugins/kimal3-plan/hooks/register.tsx +22 -0
- package/claude-plugins/kimal3-plan/hooks/spec-text.ts +45 -0
- package/claude-plugins/kimal3-plan/types/index.d.ts +10 -0
- package/coach/de/kimal3-coach.md +41 -1
- package/coach/en/kimal3-coach.md +41 -1
- package/dist/argumente.js +12 -0
- package/dist/atomar.js +22 -0
- package/dist/befehle/einrichten.js +4 -4
- package/dist/befehle/login.js +2 -2
- package/dist/befehle/plan-hoch.js +8 -0
- package/dist/befehle/plan-melde.js +105 -0
- package/dist/befehle/plan-senden.js +149 -0
- package/dist/befehle/plan.js +45 -10
- package/dist/befehle/projekt-verbinden.js +141 -0
- package/dist/befehle/skill-install.js +2 -2
- package/dist/befehle-katalog-en.js +6 -6
- package/dist/befehle-katalog.js +8 -7
- package/dist/befehlswoerter.js +9 -0
- package/dist/einstellungen-schreiben.js +1 -1
- package/dist/kimal3.js +22 -0
- package/dist/meldungen-speicher.js +189 -0
- package/dist/{logo-ablegen.js → mods-ablegen.js} +29 -18
- package/dist/plugins-mischen.js +7 -2
- package/dist/projekt-ordner.js +189 -0
- package/dist/texte/einrichten.js +3 -1
- package/dist/texte/meldungen.js +127 -0
- package/dist/texte/plan.js +42 -22
- package/dist/texte/skill.js +12 -12
- package/package.json +1 -1
- package/skill/commands/k3feierabend.md +29 -0
- package/skill/commands/k3plan.md +167 -213
- package/skill/en/commands/k3plan.md +174 -222
- package/skill/en/commands/k3show.md +3 -1
- package/skill/en/commands/k3wrapup.md +28 -0
package/README.md
CHANGED
|
@@ -42,8 +42,11 @@ kimal3 kontext fragen Die Fragen über deine Firma beantworten — Claude f
|
|
|
42
42
|
kimal3 kontext hoch Deine lokalen Änderungen an den fünf Firmen-Texten hochschicken
|
|
43
43
|
kimal3 projekt neu … Ein Projekt ins Erfolgsprofil eintragen
|
|
44
44
|
kimal3 projekt liste Deine eingetragenen Projekte anzeigen
|
|
45
|
-
kimal3 plan Planung
|
|
46
|
-
kimal3 plan
|
|
45
|
+
kimal3 plan Den Stand der Planung holen und Änderungen melden
|
|
46
|
+
kimal3 plan melde Eine Änderung an die Projektseite melden (Meldung als JSON von der Eingabe)
|
|
47
|
+
kimal3 plan senden Gemerkte Meldungen nachschicken, die offline nicht ankamen
|
|
48
|
+
kimal3 projekt verbinden Den Projektordner mit seinem Projekt in der App verbinden
|
|
49
|
+
kimal3 plan hoch Nur noch mit --ueberschreiben: den ganzen Stand durch plan/stand.json ersetzen
|
|
47
50
|
kimal3 kurs Zeigen, wo du im Mini-Kurs stehst
|
|
48
51
|
kimal3 firma daten Die Daten deiner Firma holen, damit Claude sie auswerten kann
|
|
49
52
|
kimal3 zeigen start Eine Aufnahme starten, während du eine Aufgabe vormachst
|
|
@@ -184,8 +187,11 @@ kimal3 context questions Answer the questions about your company — Claude tak
|
|
|
184
187
|
kimal3 context upload Upload your local changes to the five company texts
|
|
185
188
|
kimal3 project new … Add a project to your success profile
|
|
186
189
|
kimal3 project list Show the projects you've added
|
|
187
|
-
kimal3 plan
|
|
188
|
-
kimal3 plan
|
|
190
|
+
kimal3 plan Fetch the planning state and report changes
|
|
191
|
+
kimal3 plan report Report a change to the project page (report as JSON from the input)
|
|
192
|
+
kimal3 plan send Send kept reports that didn't arrive while offline
|
|
193
|
+
kimal3 project connect Connect the project folder with its project in the app
|
|
194
|
+
kimal3 plan upload Only with --overwrite: replace the whole state with plan/stand.json
|
|
189
195
|
kimal3 course Show where you are in the mini-course
|
|
190
196
|
kimal3 company data Fetch your company's data so Claude can analyse it
|
|
191
197
|
kimal3 show start Start a recording while you show a task
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"owner": { "name": "KImal3", "url": "https://kimal3.de" },
|
|
4
4
|
"description": "Eingetragen vom Programm kimal3 · Added by the kimal3 program",
|
|
5
5
|
"plugins": [
|
|
6
|
-
{ "name": "kimal3-logo", "source": "./kimal3-logo", "description": "KImal3-Logo über dem Eingabefeld · KImal3 logo above the prompt" }
|
|
6
|
+
{ "name": "kimal3-logo", "source": "./kimal3-logo", "description": "KImal3-Logo über dem Eingabefeld · KImal3 logo above the prompt" },
|
|
7
|
+
{ "name": "kimal3-plan", "source": "./kimal3-plan", "description": "Hält das Board der Projektseite aktuell · Keeps the project page board current" }
|
|
7
8
|
]
|
|
8
9
|
}
|
package/claude-plugins/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Diesen Ordner legt `kimal3 einrichten` an und ersetzt ihn bei jedem Lauf. Eigene Änderungen hier gehen dabei verloren.
|
|
4
4
|
Das Logo abschalten: in Claude unter **Plugins** `kimal3-logo` ausschalten — `kimal3` schaltet es danach nicht wieder ein.
|
|
5
|
+
`kimal3-plan` hält das Board der Projektseite in der App aktuell (Punkt, solange die Sitzung arbeitet; Spec-Texte). Abschalten genauso: unter **Plugins** `kimal3-plan` ausschalten.
|
|
5
6
|
|
|
6
7
|
`kimal3 setup` creates this folder and replaces it on every run; your own changes here are lost.
|
|
7
8
|
To turn the logo off: switch off `kimal3-logo` under **Plugins** in Claude — `kimal3` won't switch it back on.
|
|
9
|
+
`kimal3-plan` keeps the board on the app's project page current (dot while the session works; spec texts). Turn it off the same way: switch off `kimal3-plan` under **Plugins**.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kimal3-logo",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "KImal3-Logo über dem Eingabefeld, öffnet beim Start das Projekt in der App, benennt den Chat nach seinem Thema · KImal3 logo above the prompt, opens the project in the app on start, names the chat after its topic",
|
|
5
5
|
"author": { "name": "KImal3", "url": "https://kimal3.de" },
|
|
6
6
|
"types": "./types/index.d.ts"
|
|
@@ -16,7 +16,10 @@ const ANSICHT = 'ccd_view'
|
|
|
16
16
|
const EINBLENDEN = 'mcp__ccd_view__show_pane'
|
|
17
17
|
const geoeffnet = atom({ plugin: 'kimal3-logo', key: 'appGeoeffnet' } as const, false)
|
|
18
18
|
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Die Adresse für den Inhalt von `plan/projekt.json` oder `plan/stand.json` (`null`: keine Datei) -- beide tragen die
|
|
21
|
+
* Nummer als `projekt_id`. Ohne gültige Nummer der Kurs, wie der Link in der Leiste (S-3, S-4).
|
|
22
|
+
*/
|
|
20
23
|
export function adresseFuer(standJson: string | null): string {
|
|
21
24
|
if (standJson !== null) {
|
|
22
25
|
try {
|
|
@@ -30,6 +33,21 @@ export function adresseFuer(standJson: string | null): string {
|
|
|
30
33
|
return `${APP}/kurs`
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Ist das ein gueltiger Ordnerzettel (`plan/projekt.json`: `format: 1`, Projektnummer, Titel)? Dieselbe Pruefung wie
|
|
38
|
+
* `zettelAus` im Mod `kimal3-plan` und in `cli/src/projekt-ordner.ts` -- ein ungueltiger zaehlt nicht, dann gilt die
|
|
39
|
+
* alte `plan/stand.json`.
|
|
40
|
+
*/
|
|
41
|
+
export function istZettel(text: string | null): boolean {
|
|
42
|
+
if (text === null) return false
|
|
43
|
+
try {
|
|
44
|
+
const z = JSON.parse(text.replace(/^\uFEFF/, '')) as { format?: unknown; projekt_id?: unknown; titel?: unknown } | null
|
|
45
|
+
return z?.format === 1 && typeof z.projekt_id === 'string' && PROJEKTNUMMER.test(z.projekt_id) && typeof z.titel === 'string' && z.titel.trim() !== ''
|
|
46
|
+
} catch {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
/** Die eigene Erlaubnis (unten): nur der Aufruf dieses Mods, nur eine Adresse der App. Herausgelöst, damit `check:logo` die Grenze prüft. */
|
|
34
52
|
export function istEigenerAufruf(plugin: string, input: unknown): boolean {
|
|
35
53
|
const url = (input as { url?: unknown } | null)?.url
|
|
@@ -83,29 +101,39 @@ async function liesDatei($: Engine, pfad: string): Promise<string | null> {
|
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
/**
|
|
86
|
-
*
|
|
104
|
+
* Eine Datei des Projekts, in dem Claude läuft (S-5, S-24, S-25): vom Startordner nach oben, bis zur Wurzel des
|
|
87
105
|
* eigenen Git-Projekts und nie darüber hinaus — sonst bekäme ein Projekt ohne Plan die Seite eines anderen. Ist die
|
|
88
|
-
* Wurzel eine Arbeitskopie (`.git` ist dort eine Datei), zählt danach die
|
|
106
|
+
* Wurzel eine Arbeitskopie (`.git` ist dort eine Datei), zählt danach die Datei ihres Hauptprojekts.
|
|
89
107
|
*/
|
|
90
|
-
async function
|
|
108
|
+
async function findePlanDatei($: Engine, cwd: string, datei: string): Promise<string | null> {
|
|
91
109
|
for (const ordner of ordnerNachOben(cwd)) {
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
110
|
+
const text = await liesDatei($, `${ordner}/${datei}`)
|
|
111
|
+
if (text !== null) return text
|
|
94
112
|
if (!(await $.fs.exists(`${ordner}/.git`))) continue
|
|
95
113
|
// Ein Ordner `.git` lässt sich nicht lesen: echte Projektwurzel, hier ist Schluss.
|
|
96
114
|
const gitdatei = await liesDatei($, `${ordner}/.git`)
|
|
97
115
|
const haupt = gitdatei === null ? null : hauptordnerAusGitdatei(gitdatei)
|
|
98
|
-
return haupt === null ? null : liesDatei($, `${haupt}
|
|
116
|
+
return haupt === null ? null : liesDatei($, `${haupt}/${datei}`)
|
|
99
117
|
}
|
|
100
118
|
return null
|
|
101
119
|
}
|
|
102
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Erst der Ordnerzettel `plan/projekt.json`, dann `plan/stand.json` für alte Ordner (Spec 2026-10-23-brett-in-echtzeit,
|
|
123
|
+
* 1.4, US-8) — dieselbe Reihenfolge wie `kimal3` (`cli/src/projekt-ordner.ts`). Ein ungültiger Zettel zählt nicht.
|
|
124
|
+
*/
|
|
125
|
+
async function adresseJetzt($: Engine): Promise<string> {
|
|
126
|
+
const cwd = await $.session.cwd()
|
|
127
|
+
const zettel = await findePlanDatei($, cwd, 'plan/projekt.json')
|
|
128
|
+
return adresseFuer(istZettel(zettel) ? zettel : await findePlanDatei($, cwd, 'plan/stand.json'))
|
|
129
|
+
}
|
|
130
|
+
|
|
103
131
|
async function oeffne($: Engine): Promise<boolean> {
|
|
104
|
-
const
|
|
132
|
+
const adresse = await adresseJetzt($)
|
|
105
133
|
// Scheitern meldet sich als Scheitern, nie im Chat (S-13): eine Zeile nur im Debug-Protokoll (`to: 'debug'` —
|
|
106
134
|
// ohne steht sie im Chatverlauf, Befund der Durchsicht 23.10.).
|
|
107
135
|
try {
|
|
108
|
-
const antwort = await $.mcp.call(BROWSER, 'navigate', { url:
|
|
136
|
+
const antwort = await $.mcp.call(BROWSER, 'navigate', { url: adresse })
|
|
109
137
|
if (!antwort.isError) {
|
|
110
138
|
await einblenden($)
|
|
111
139
|
return true
|
|
@@ -6,7 +6,7 @@ import { updateClaudeFragen, updateInstallieren, updateZustand } from './update'
|
|
|
6
6
|
import { satzWeicht, TEXTE, type Zustand } from './update-rechnung'
|
|
7
7
|
|
|
8
8
|
// Die Sprache des Kontos. `kimal3 einrichten` setzt diese Zeile beim Ablegen in `~/.kimal3/claude-plugins` und prüft
|
|
9
|
-
// danach, dass sie gesetzt ist (`cli/src/
|
|
9
|
+
// danach, dass sie gesetzt ist (`cli/src/mods-ablegen.ts`).
|
|
10
10
|
const SPRACHE: 'de' | 'en' = 'de'
|
|
11
11
|
// Fassung und Orte von `kimal3` auf diesem Rechner; beim Ablegen gesetzt wie die Sprache (hooks/ablage.ts).
|
|
12
12
|
const ABLAGE: Ablage = KEINE_ABLAGE
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kimal3-plan",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Hält das Board der Projektseite in der App aktuell: Punkt solange die Sitzung arbeitet, Spec-Texte, hält Claude vor dem ersten Bau-Schritt an · Keeps the board on the app's project page current: dot while the session works, spec texts, stops Claude before the first build step",
|
|
5
|
+
"author": { "name": "KImal3", "url": "https://kimal3.de" },
|
|
6
|
+
"types": "./types/index.d.ts"
|
|
7
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { atom, read, update, type EngineInterface, type On } from 'claude-code'
|
|
2
|
+
import { TEXTE, type Lauf } from './lauf'
|
|
3
|
+
import { relativImOrdner } from './ordner'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Die drei Saetze, die NUR Claude bekommt (Spec 1.3 Aufgabe 3, 1.5, 1.7, S-25, S-26, US-6, US-8; Bauplan R-4).
|
|
7
|
+
* Der Mensch sieht keinen davon (gemessen M-3, Claude-App). Zwei Wege, je nachdem, ob der Satz eine Runde starten soll:
|
|
8
|
+
*
|
|
9
|
+
* - **Sperre vor dem ersten Bau-Schritt** (US-6, Dennis 25.10.: „Ich will das Problem gleich gefixt haben“): Will Claude
|
|
10
|
+
* eine Datei aendern, ohne dass diese Sitzung eine Story „in Arbeit“ hat, lehnt der Mod die Aenderung VORHER ab
|
|
11
|
+
* (`deny`), und Claude liest den Satz als Grund. Bis zum 25.10. kam der Satz erst NACH der Aenderung als `context` --
|
|
12
|
+
* gemessen in einer echten Sitzung: Claude schrieb zuerst die Pruefung und meldete danach. Der zweite Versuch geht
|
|
13
|
+
* durch (30-Minuten-Sperre): Eine Aenderung ohne Story (Statuszettel, neue Spec) haengt so nie fest. Nur die
|
|
14
|
+
* Hauptsitzung: Ein Subagent (`e.agentId`) baut im Auftrag und meldet nicht. Nur in einem Projektordner mit
|
|
15
|
+
* Ordnerzettel (S-30), und nur, wenn die App zuletzt „nichts in Arbeit“ gesagt hat -- weiss der Mod es nicht (noch
|
|
16
|
+
* keine Antwort, App weg), haelt er nichts an. 🔴 Scheitert der Hook selbst, ueberspringt ihn die Maschine und die
|
|
17
|
+
* Aenderung laeuft (`tool.call`, Typen: „A guard that fails is skipped and the tool runs“) -- so gewollt: Ein Fehler
|
|
18
|
+
* im Mod darf die Arbeit nie aufhalten.
|
|
19
|
+
* - **Anstoss „Projekt anlegen“** (US-8) und **„gemerkte Meldungen senden“** (R-4) sind eine Zeile im Gespraech
|
|
20
|
+
* (`$.session.append`): Claude liest sie sofort und handelt.
|
|
21
|
+
*/
|
|
22
|
+
const SPERRE_MS = 30 * 60_000
|
|
23
|
+
|
|
24
|
+
// In `$.state`, weil ein Neuladen des Mods die Variablen des Moduls zuruecksetzt -- die Sperre und der einmalige Anstoss
|
|
25
|
+
// sollen das ueberstehen.
|
|
26
|
+
const letzteErinnerung = atom({ plugin: 'kimal3-plan', key: 'letzteErinnerung' } as const, null)
|
|
27
|
+
const angestossen = atom({ plugin: 'kimal3-plan', key: 'anstossGegeben' } as const, false)
|
|
28
|
+
|
|
29
|
+
/** Die Datei eines Schreib-Werkzeugs (`file_path`, bei Notebooks `notebook_path`). */
|
|
30
|
+
export function dateiAus(e: unknown): string | null {
|
|
31
|
+
const o = e as { file_path?: unknown; notebook_path?: unknown }
|
|
32
|
+
const datei = o.file_path ?? o.notebook_path
|
|
33
|
+
return typeof datei === 'string' && datei !== '' ? datei : null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function erinnerung(on: On, lauf: Lauf): void {
|
|
37
|
+
on('tool.call', { tool: ['Write', 'Edit', 'NotebookEdit'] }, async ($, e, next) => {
|
|
38
|
+
const satz = e.agentId === undefined ? await satzFuer($, lauf, dateiAus(e)) : null
|
|
39
|
+
return satz === null ? next(e) : { deny: satz }
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function satzFuer($: EngineInterface, lauf: Lauf, datei: string | null): Promise<string | null> {
|
|
44
|
+
const lage = lauf.lage
|
|
45
|
+
if (lauf.inArbeit !== false || lage.art !== 'projekt' || lage.zettel === null || datei === null) return null
|
|
46
|
+
// Markdown ist Planung (Spec, Bauplan, Statuszettel), kein Bau-Schritt: Eine neue Spec entsteht, bevor es eine Story gibt.
|
|
47
|
+
if (datei.toLowerCase().endsWith('.md')) return null
|
|
48
|
+
if (relativImOrdner(datei, lage.ordner, await $.session.cwd()) === null) return null
|
|
49
|
+
const jetzt = await $.clock.now()
|
|
50
|
+
const zuletzt = await read($, letzteErinnerung)
|
|
51
|
+
if (zuletzt !== null && jetzt - zuletzt < SPERRE_MS) return null
|
|
52
|
+
await update($, letzteErinnerung, () => jetzt)
|
|
53
|
+
return TEXTE[lauf.sprache].erinnerung(lage.zettel.titel)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Projektordner ohne Ordnerzettel: einmal je Sitzung „leg das Projekt an“ (US-8). */
|
|
57
|
+
export async function anstossOhneZettel($: EngineInterface, lauf: Lauf): Promise<void> {
|
|
58
|
+
if (lauf.lage.art !== 'projekt' || lauf.lage.zettel !== null || (await read($, angestossen))) return
|
|
59
|
+
if (await anClaude($, TEXTE[lauf.sprache].anstoss)) await update($, angestossen, () => true)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Liegen gemerkte Meldungen (`~/.kimal3/meldungen/*.json`; `.tmp` sind Reste) und ist die App erreichbar: senden lassen. */
|
|
63
|
+
export async function gemerkteMeldungenAnstossen($: EngineInterface, lauf: Lauf): Promise<void> {
|
|
64
|
+
if (lauf.heim === null) return
|
|
65
|
+
const eintraege = await $.fs.list(`${lauf.heim}/.kimal3/meldungen`).catch(() => [])
|
|
66
|
+
if (eintraege.some((d: { kind: string; name: string }) => d.kind === 'file' && d.name.endsWith('.json'))) await anClaude($, TEXTE[lauf.sprache].gemerkt)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Nach `/clear` beginnt eine neue Sitzung: ihre eigene Sperre, ihr eigener Anstoss. */
|
|
70
|
+
export async function sitzungNeu($: EngineInterface): Promise<void> {
|
|
71
|
+
await update($, letzteErinnerung, () => null)
|
|
72
|
+
await update($, angestossen, () => false)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function anClaude($: EngineInterface, text: string): Promise<boolean> {
|
|
76
|
+
try {
|
|
77
|
+
await $.session.append({ message: { type: 'user', content: [{ type: 'text', text }] } })
|
|
78
|
+
return true
|
|
79
|
+
} catch (fehler) {
|
|
80
|
+
$.ui.log(`kimal3-plan: Satz an Claude nicht angehaengt: ${String(fehler).slice(0, 200)}`, { to: 'debug' })
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { EngineInterface } from 'claude-code'
|
|
2
|
+
import { TEXTE, type Fehler, type Lauf } from './lauf'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Die eine Fehlerzeile des Mods (Spec 1.3, 1.5, US-4, S-11, S-22; Bauplan R-8): in der Statuszeile unter dem
|
|
6
|
+
* Eingabefeld, weil eine farbige Zeile dort in der Claude-App gar nicht erscheint (gemessen M-4). Sie steht, solange der
|
|
7
|
+
* Fehler besteht, und verschwindet danach -- `$.ui.status` wird nur gerufen, wenn sich der Satz AENDERT, so bleibt es
|
|
8
|
+
* bei zehn gescheiterten Versuchen bei EINER Zeile. Bei mehreren Fehlern zeigt sie den ersten nach diesem Vorrang:
|
|
9
|
+
* Wer nicht angemeldet ist, dem hilft „nicht erreichbar“ nichts.
|
|
10
|
+
*/
|
|
11
|
+
const VORRANG: readonly Fehler[] = ['nicht-angemeldet', 'kimal3-fehlt', 'nicht-erreichbar']
|
|
12
|
+
|
|
13
|
+
function zeige($: EngineInterface, lauf: Lauf): void {
|
|
14
|
+
const art = VORRANG.find(f => lauf.fehler.has(f))
|
|
15
|
+
const satz = art === undefined ? undefined : TEXTE[lauf.sprache].fehler[art]
|
|
16
|
+
if (satz === lauf.zeile) return
|
|
17
|
+
lauf.zeile = satz
|
|
18
|
+
$.ui.status(satz)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Die Fehler nach einem Lebenszeichen: Es prueft Ausweis UND App, sein Ergebnis ersetzt alles Bisherige. */
|
|
22
|
+
export function fehlerSind($: EngineInterface, lauf: Lauf, fehler: readonly Fehler[]): void {
|
|
23
|
+
lauf.fehler = new Set(fehler)
|
|
24
|
+
merkeAusfall(lauf)
|
|
25
|
+
zeige($, lauf)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Einzelne Fehler kommen hinzu (`true`) oder sind behoben (`false`), etwa nach dem Hochladen eines Spec-Texts. */
|
|
29
|
+
export function fehlerSetzen($: EngineInterface, lauf: Lauf, aenderung: Partial<Record<Fehler, boolean>>): void {
|
|
30
|
+
for (const [art, besteht] of Object.entries(aenderung) as [Fehler, boolean][]) {
|
|
31
|
+
if (besteht) lauf.fehler.add(art)
|
|
32
|
+
else lauf.fehler.delete(art)
|
|
33
|
+
}
|
|
34
|
+
merkeAusfall(lauf)
|
|
35
|
+
zeige($, lauf)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** War die App weg, koennen sich in der Zeit Meldungen angesammelt haben: nach der Rueckkehr nachsehen (R-4). */
|
|
39
|
+
function merkeAusfall(lauf: Lauf): void {
|
|
40
|
+
if (lauf.fehler.has('nicht-erreichbar')) lauf.meldungenPruefen = true
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "modules": ["./register.tsx"] }
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { EngineInterface, Timer } from 'claude-code'
|
|
2
|
+
import { liesDatei, normiert, type Ordnerlage } from './ordner'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Was die Teile des Mods gemeinsam wissen (ein Objekt je Laden des Mods, angelegt in `register.tsx`), dazu die Saetze in
|
|
6
|
+
* beiden Sprachen und der eine Weg zur App.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type Sprache = 'de' | 'en'
|
|
10
|
+
export type Fehler = 'nicht-angemeldet' | 'kimal3-fehlt' | 'nicht-erreichbar'
|
|
11
|
+
|
|
12
|
+
export type Lauf = {
|
|
13
|
+
/** Die SPRACHE-Zeile aus `register.tsx` -- gilt, wenn `~/.kimal3/sprache.json` fehlt (S-23). */
|
|
14
|
+
vorgabe: Sprache
|
|
15
|
+
sprache: Sprache
|
|
16
|
+
/** Der Benutzerordner mit `/` (`USERPROFILE`, sonst `HOME`) -- dort liegt `.kimal3/` (M-5). */
|
|
17
|
+
heim: string | null
|
|
18
|
+
basis: string
|
|
19
|
+
lage: Ordnerlage
|
|
20
|
+
/** Die letzte Antwort der App auf ein Lebenszeichen: `null`, solange keine gekommen ist (R-5). */
|
|
21
|
+
inArbeit: boolean | null
|
|
22
|
+
/** Zaehlt jede Meldung, die Claude gerade abgeschickt hat: Ein Lebenszeichen, das davor losging, setzt `inArbeit` nicht. */
|
|
23
|
+
meldungen: number
|
|
24
|
+
/** Die Spec-Dateien des Projekts aus dem letzten Lebenszeichen -- nur deren Texte gehen hoch (Datensparsamkeit). */
|
|
25
|
+
specDateien: readonly string[]
|
|
26
|
+
takt: Timer | null
|
|
27
|
+
/** Ein Start laeuft gerade (zwischen erstem `await` und dem Zeitgeber): ein zweiter wartet nicht, er unterbleibt. */
|
|
28
|
+
startet: boolean
|
|
29
|
+
/** Zaehlt jedes Sitzungsende hoch: Eine Antwort, die fuer eine fruehere Sitzung unterwegs war, wirkt nicht mehr. */
|
|
30
|
+
generation: number
|
|
31
|
+
/** `session.end` ist gelaufen: Ein Lebenszeichen, das noch unterwegs war, darf danach nichts mehr tun. */
|
|
32
|
+
beendet: boolean
|
|
33
|
+
/** Fuer diese Projektnummer hat die App `403` gesagt (nicht Eigentuemer, geloescht): nicht weiter fragen. */
|
|
34
|
+
abgelehnt: string | null
|
|
35
|
+
/** Fuer dieses Projekt ging ein Lebenszeichen raus -- nur dann gibt es etwas abzumelden. */
|
|
36
|
+
gemeldetFuer: string | null
|
|
37
|
+
/** Beim naechsten erfolgreichen Lebenszeichen nach gemerkten Meldungen sehen (R-4): beim Start und nach jedem Ausfall. */
|
|
38
|
+
meldungenPruefen: boolean
|
|
39
|
+
fehler: Set<Fehler>
|
|
40
|
+
/** Was gerade in der Statuszeile steht (`undefined`: nichts; `null`: noch nie gesetzt -- die erste Auswertung nach dem
|
|
41
|
+
* Laden ruft `$.ui.status` in jedem Fall und raeumt so eine Zeile ab, die ein frueherer Lauf stehen liess). */
|
|
42
|
+
zeile: string | undefined | null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function neuerLauf(vorgabe: Sprache): Lauf {
|
|
46
|
+
return {
|
|
47
|
+
vorgabe, sprache: vorgabe, heim: null, basis: APP, lage: { art: 'kein-projektordner' }, inArbeit: null, meldungen: 0, specDateien: [],
|
|
48
|
+
takt: null, startet: false, generation: 0, beendet: false, abgelehnt: null, gemeldetFuer: null, meldungenPruefen: true,
|
|
49
|
+
fehler: new Set(), zeile: null,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const APP = 'https://app.kimal3.de'
|
|
54
|
+
|
|
55
|
+
// Wortlaut: Spec 1.3 (Erinnerung), 1.7 (Anstoss), S-11, S-22, US-4; Bauplan R-4 (gemerkte Meldungen).
|
|
56
|
+
// Englische Befehlswoerter wie in `cli/src/befehlswoerter.ts` (`project connect`, `plan send`).
|
|
57
|
+
export const TEXTE = {
|
|
58
|
+
de: {
|
|
59
|
+
erinnerung: (titel: string) => `Halt, die Datei ist noch nicht geändert: Du arbeitest im Projekt ${titel}, aber diese Sitzung hat keine Story ‚in Arbeit‘ gemeldet. Melde zuerst die Story, an der du arbeitest, und ändere die Datei danach. Gehört die Änderung zu keiner Story, notiere sie als Thema oder versuch es einfach noch einmal.`,
|
|
60
|
+
anstoss: 'Für diesen Projektordner gibt es noch kein Projekt in der App — leg es mit `kimal3 projekt verbinden` an.',
|
|
61
|
+
gemerkt: 'Gemerkte Meldungen warten — führe `kimal3 plan senden` aus.',
|
|
62
|
+
fehler: {
|
|
63
|
+
'nicht-erreichbar': 'Die Projektseite ist gerade nicht erreichbar — ich versuche es gleich wieder.',
|
|
64
|
+
'nicht-angemeldet': 'Du bist bei kimal3 nicht mehr angemeldet — `kimal3 login`.',
|
|
65
|
+
'kimal3-fehlt': 'kimal3 fehlt oder ist zu alt — `npm install -g kimal3@latest`.',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
en: {
|
|
69
|
+
erinnerung: (titel: string) => `Stop, the file has not been changed: You are working in the project ${titel}, but this session hasn't reported a story as ‘in progress’. Report the story you're working on first, then change the file. If the change belongs to no story, note it as a topic or simply try again.`,
|
|
70
|
+
anstoss: 'There is no project in the app for this project folder yet — create it with `kimal3 project connect`.',
|
|
71
|
+
gemerkt: 'Saved reports are waiting — run `kimal3 plan send`.',
|
|
72
|
+
fehler: {
|
|
73
|
+
'nicht-erreichbar': "The project page can't be reached right now — I'll try again shortly.",
|
|
74
|
+
'nicht-angemeldet': "You're no longer signed in to kimal3 — `kimal3 login`.",
|
|
75
|
+
'kimal3-fehlt': 'kimal3 is missing or too old — `npm install -g kimal3@latest`.',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
} as const
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Benutzerordner, Sprache und App-Adresse -- bei jedem Sitzungsstart neu, damit eine umgestellte Kontosprache ohne neues
|
|
82
|
+
* `kimal3 einrichten` gilt (S-23). `KIMAL3_URL` wie in `cli/src/anmeldung.ts` (Messen gegen den lokalen Server).
|
|
83
|
+
*/
|
|
84
|
+
export async function umgebungLesen($: EngineInterface, lauf: Lauf): Promise<void> {
|
|
85
|
+
const heim = (await $.env.get('USERPROFILE')) ?? (await $.env.get('HOME'))
|
|
86
|
+
lauf.heim = heim === undefined || heim === '' ? null : normiert(heim)
|
|
87
|
+
lauf.basis = ((await $.env.get('KIMAL3_URL')) ?? APP).replace(/\/+$/, '')
|
|
88
|
+
lauf.sprache = lauf.vorgabe
|
|
89
|
+
const text = lauf.heim === null ? null : await liesDatei($, `${lauf.heim}/.kimal3/sprache.json`)
|
|
90
|
+
try {
|
|
91
|
+
const s: unknown = text === null ? null : JSON.parse(text.replace(/^\uFEFF/, ''))?.sprache
|
|
92
|
+
if (s === 'de' || s === 'en') lauf.sprache = s
|
|
93
|
+
} catch {
|
|
94
|
+
// kaputte Datei: die SPRACHE-Zeile gilt
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Der Geraeteschluessel aus `~/.kimal3/anmeldung.json` (M-5), bei jedem Gebrauch frisch gelesen -- so wirkt ein
|
|
100
|
+
* `kimal3 login` sofort. 🔴 Der Schluessel wird nur benutzt, nie ausgegeben oder protokolliert.
|
|
101
|
+
* Ohne Schluessel: Gibt es `~/.kimal3/`, ist `kimal3` da und nur abgemeldet (S-11, `kimal3 login`); fehlt der Ordner ganz,
|
|
102
|
+
* fehlt `kimal3` (S-22, installieren) -- Ruling der Fixrunde.
|
|
103
|
+
*/
|
|
104
|
+
export async function liesAusweis($: EngineInterface, lauf: Lauf): Promise<{ schluessel: string } | { fehler: Fehler }> {
|
|
105
|
+
if (lauf.heim === null) return { fehler: 'kimal3-fehlt' }
|
|
106
|
+
const text = await liesDatei($, `${lauf.heim}/.kimal3/anmeldung.json`)
|
|
107
|
+
try {
|
|
108
|
+
const s: unknown = text === null ? null : JSON.parse(text.replace(/^\uFEFF/, ''))?.schluessel
|
|
109
|
+
if (typeof s === 'string' && s !== '') return { schluessel: s }
|
|
110
|
+
} catch {
|
|
111
|
+
// kaputte Datei: wie keine
|
|
112
|
+
}
|
|
113
|
+
const ordnerDa = await $.fs.exists(`${lauf.heim}/.kimal3`).catch(() => false)
|
|
114
|
+
return { fehler: ordnerDa ? 'nicht-angemeldet' : 'kimal3-fehlt' }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Wie lange ein Abruf an die App hoechstens dauern darf: `$.http.fetch` kennt keine eigene Zeitgrenze. */
|
|
118
|
+
const ZEITGRENZE_MS = 8_000
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Ein Abruf an `/api/mod/*`. `null`: die App war nicht zu erreichen (kein Netz, abgelehnt, oder keine Antwort innerhalb
|
|
122
|
+
* von `grenzeMs`). 🔴 Ohne die Zeitgrenze haengt ein Abruf, der nie antwortet, das Lebenszeichen fuer immer auf --
|
|
123
|
+
* still, ohne Statuszeile (Befund der Durchsicht).
|
|
124
|
+
*/
|
|
125
|
+
export function anApp($: EngineInterface, lauf: Lauf, pfad: string, schluessel: string, rumpf: unknown, grenzeMs = ZEITGRENZE_MS): Promise<{ status: number; text: string } | null> {
|
|
126
|
+
return new Promise(fertig => {
|
|
127
|
+
const frist = $.clock.after(Math.max(0, Math.min(grenzeMs, ZEITGRENZE_MS)), () => fertig(null))
|
|
128
|
+
$.http.fetch(`${lauf.basis}${pfad}`, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: { Authorization: `Bearer ${schluessel}`, 'Content-Type': 'application/json' },
|
|
131
|
+
body: JSON.stringify(rumpf),
|
|
132
|
+
}).then(
|
|
133
|
+
antwort => {
|
|
134
|
+
frist.cancel()
|
|
135
|
+
fertig({ status: antwort.status, text: antwort.text })
|
|
136
|
+
},
|
|
137
|
+
() => {
|
|
138
|
+
frist.cancel()
|
|
139
|
+
fertig(null)
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { EngineInterface, On } from 'claude-code'
|
|
2
|
+
import { anstossOhneZettel, gemerkteMeldungenAnstossen, sitzungNeu } from './erinnerung'
|
|
3
|
+
import { fehlerSind } from './fehlerzeile'
|
|
4
|
+
import { anApp, liesAusweis, umgebungLesen, type Lauf } from './lauf'
|
|
5
|
+
import { lageJetzt } from './ordner'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Den Punkt am Leben halten (Spec 1.3 Aufgabe 2, E-4, US-2; Bauplan R-5, Review Focus 5). Solange die Sitzung in einem
|
|
9
|
+
* Projektordner mit Ordnerzettel offen ist, meldet sich der Mod alle 10 Sekunden bei der App (`POST
|
|
10
|
+
* /api/mod/lebenszeichen`, gemessen M-2: der Zeitgeber laeuft auch, waehrend Claude arbeitet) und merkt sich, ob diese
|
|
11
|
+
* Sitzung eine Story „in Arbeit“ hat. Beim Ende der Sitzung meldet er sich ab, damit der Punkt in unter 1 Sekunde weg
|
|
12
|
+
* ist; ein abgeschossener Prozess meldet sich nie ab, dafuer gibt es die 30-Sekunden-Regel der App.
|
|
13
|
+
*
|
|
14
|
+
* Jeder Takt liest Ordner, Zettel und Ausweis neu: So kommt ein Zettel, den Claude nach dem Anstoss anlegt (US-8), und ein
|
|
15
|
+
* `kimal3 login` beim naechsten Takt an, ohne Neustart.
|
|
16
|
+
*/
|
|
17
|
+
const TAKT_MS = 10_000
|
|
18
|
+
const MELDUNG = /\bplan\s+(melde|report|senden|send)\b/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Die Nummer dieser Sitzung fuer `kimal3 plan melde` (Ruling Final, Important 2): Ob `CLAUDE_CODE_SESSION_ID` nach
|
|
22
|
+
* `/clear` neu ist, ist ungemessen -- der Mod setzt deshalb bei jedem Start, auch nach `/clear`, `KIMAL3_SITZUNG`, und
|
|
23
|
+
* das Programm nimmt sie vor Claudes eigener Variable (die bleibt unberuehrt). Gemessen am 23.10.: die Variable kommt
|
|
24
|
+
* bei einem Bash-Aufruf an (docs/qa/2026-10-23-brett-echtzeit-messungen.md, „Final — Sitzungskennung“). Scheitert das
|
|
25
|
+
* Setzen, laeuft der Mod weiter; das Programm faellt dann auf `CLAUDE_CODE_SESSION_ID` zurueck.
|
|
26
|
+
*/
|
|
27
|
+
async function sitzungFuerProgramm($: EngineInterface): Promise<void> {
|
|
28
|
+
await $.env.set('KIMAL3_SITZUNG', await $.session.id()).catch(() => undefined)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function lebenszeichen(on: On, lauf: Lauf): void {
|
|
32
|
+
on('session.start', async ($, e, next) => {
|
|
33
|
+
const ergebnis = await next(e)
|
|
34
|
+
await sitzungFuerProgramm($)
|
|
35
|
+
await starte($, lauf)
|
|
36
|
+
return ergebnis
|
|
37
|
+
})
|
|
38
|
+
// Haengt sich eine Oberflaeche an, ohne dass es lief (etwa nach einem frueheren Fehlstart): noch einmal versuchen.
|
|
39
|
+
on('session.attach', async ($, e, next) => {
|
|
40
|
+
const ergebnis = await next(e)
|
|
41
|
+
await starte($, lauf)
|
|
42
|
+
return ergebnis
|
|
43
|
+
})
|
|
44
|
+
// `/clear` beendet die Sitzung (abgemeldet unten), der Prozess laeuft unter einer neuen Kennung weiter -- ohne
|
|
45
|
+
// `session.start`. Die neue Sitzung meldet sich deshalb hier wieder an.
|
|
46
|
+
on('classic.SessionStart', async ($, e, next) => {
|
|
47
|
+
const ergebnis = await next(e)
|
|
48
|
+
if (e.source === 'clear') {
|
|
49
|
+
Object.assign(lauf, { beendet: false, inArbeit: null, specDateien: [], abgelehnt: null, gemeldetFuer: null, meldungenPruefen: true })
|
|
50
|
+
await sitzungNeu($)
|
|
51
|
+
await sitzungFuerProgramm($)
|
|
52
|
+
await starte($, lauf)
|
|
53
|
+
}
|
|
54
|
+
return ergebnis
|
|
55
|
+
})
|
|
56
|
+
// Claude hat gerade gemeldet (`kimal3 plan melde`/`plan senden`): Was der Mod ueber „in Arbeit“ weiss, ist ab jetzt
|
|
57
|
+
// veraltet -- bis zu 10 s lang kaeme sonst die Erinnerung, obwohl die Meldung schon raus ist (Final-Review, Important 1).
|
|
58
|
+
// Also gilt es als unbekannt (keine Erinnerung), und ein Lebenszeichen holt sofort den neuen Stand. Ein Fehlalarm des
|
|
59
|
+
// Musters kostet nur ein Lebenszeichen mehr.
|
|
60
|
+
on('tool.call', { tool: ['Bash', 'PowerShell'] }, async ($, e, next) => {
|
|
61
|
+
const ergebnis = await next(e)
|
|
62
|
+
if (!MELDUNG.test(e.command)) return ergebnis
|
|
63
|
+
lauf.inArbeit = null
|
|
64
|
+
lauf.meldungen++
|
|
65
|
+
// Nur, wenn der Zeitgeber laeuft (Projektordner, nicht beendet): kein zweiter Zeitgeber, und `takt` faellt aus, wenn
|
|
66
|
+
// schon eines unterwegs ist -- dessen Antwort zaehlt dann nicht (`meldungen`), bis der naechste Takt fragt.
|
|
67
|
+
if (lauf.takt !== null) $.clock.after(0, () => void takt($, lauf))
|
|
68
|
+
return ergebnis
|
|
69
|
+
})
|
|
70
|
+
on('session.end', async ($, e, next) => {
|
|
71
|
+
// 🔴 ZUERST den Zeitgeber anhalten, DANN abmelden (Review Focus 5): sonst kann ein letzter Takt nach dem Abmelden
|
|
72
|
+
// ankommen. Die App lehnt ihn zwar ab (`410`), aber `beendet` haelt auch einen Takt an, der schon unterwegs ist.
|
|
73
|
+
lauf.beendet = true
|
|
74
|
+
lauf.generation++
|
|
75
|
+
lauf.takt?.cancel()
|
|
76
|
+
lauf.takt = null
|
|
77
|
+
lauf.inArbeit = null
|
|
78
|
+
// Die ganze Kette von `session.end` hat ~1,5 s (gemessen M-2); der Abruf endet vorher, damit Claude Code nicht
|
|
79
|
+
// mitten in ihm abschneidet und die Hooks nach uns ihre Zeit behalten.
|
|
80
|
+
const grenze = next.budget.remainingMs - 100
|
|
81
|
+
if (lauf.gemeldetFuer !== null) await abmelden($, lauf, lauf.gemeldetFuer, e.sessionId, grenze)
|
|
82
|
+
lauf.gemeldetFuer = null
|
|
83
|
+
return next(e)
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function starte($: EngineInterface, lauf: Lauf): Promise<void> {
|
|
88
|
+
// 🔴 `startet` steht VOR dem ersten `await`: `session.start` und `session.attach` kommen fast gleichzeitig, und
|
|
89
|
+
// beide bestanden sonst die Pruefung auf `takt` -- zwei Zeitgeber, einer davon nie angehalten (Befund der Durchsicht).
|
|
90
|
+
if (lauf.takt !== null || lauf.startet || lauf.beendet) return
|
|
91
|
+
lauf.startet = true
|
|
92
|
+
try {
|
|
93
|
+
await umgebungLesen($, lauf)
|
|
94
|
+
lauf.lage = await lageJetzt($)
|
|
95
|
+
// Kein Projektordner (Werkbank, Beispielordner, ausserhalb): der Mod tut nichts (S-30).
|
|
96
|
+
if (lauf.lage.art !== 'projekt') return
|
|
97
|
+
await anstossOhneZettel($, lauf)
|
|
98
|
+
lauf.takt = $.clock.every(TAKT_MS, () => void takt($, lauf))
|
|
99
|
+
// Das erste Lebenszeichen sofort (Punkt in unter 1 s, E-3), aber ohne den Sitzungsstart darauf warten zu lassen.
|
|
100
|
+
$.clock.after(0, () => void takt($, lauf))
|
|
101
|
+
} finally {
|
|
102
|
+
lauf.startet = false
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Die Sitzung (`generation`), fuer die gerade ein Lebenszeichen unterwegs ist -- je Sitzung hoechstens eines. */
|
|
107
|
+
let unterwegs: number | null = null
|
|
108
|
+
|
|
109
|
+
async function takt($: EngineInterface, lauf: Lauf): Promise<void> {
|
|
110
|
+
// Ein Takt, dessen Vorgaenger noch auf die App wartet (langsames Netz), faellt aus, statt sich zu stapeln.
|
|
111
|
+
const generation = lauf.generation
|
|
112
|
+
if (lauf.beendet || unterwegs === generation) return
|
|
113
|
+
unterwegs = generation
|
|
114
|
+
try {
|
|
115
|
+
await einTakt($, lauf, generation)
|
|
116
|
+
} finally {
|
|
117
|
+
if (unterwegs === generation) unterwegs = null
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function einTakt($: EngineInterface, lauf: Lauf, generation: number): Promise<void> {
|
|
122
|
+
// Nach jedem `await`: Ist die Sitzung, fuer die dieser Takt lief, inzwischen vorbei (Ende, `/clear`), wirkt er nicht
|
|
123
|
+
// mehr -- sonst haelt ein spaetes `410` der alten Sitzung den Zeitgeber der neuen an.
|
|
124
|
+
const vorbei = () => lauf.beendet || lauf.generation !== generation
|
|
125
|
+
const lage = await lageJetzt($)
|
|
126
|
+
if (vorbei()) return
|
|
127
|
+
lauf.lage = lage
|
|
128
|
+
if (lage.art !== 'projekt') return
|
|
129
|
+
const ausweis = await liesAusweis($, lauf)
|
|
130
|
+
if (vorbei()) return
|
|
131
|
+
if ('fehler' in ausweis) {
|
|
132
|
+
lauf.inArbeit = null
|
|
133
|
+
return fehlerSind($, lauf, [ausweis.fehler])
|
|
134
|
+
}
|
|
135
|
+
const zettel = lage.zettel
|
|
136
|
+
// Ohne Zettel gibt es noch kein Projekt (den Anstoss bekam Claude beim Start); nach `403` fragt der Mod nicht mehr.
|
|
137
|
+
if (zettel === null || zettel.projekt_id === lauf.abgelehnt) return fehlerSind($, lauf, [])
|
|
138
|
+
|
|
139
|
+
const sitzung = await $.session.id()
|
|
140
|
+
if (vorbei()) return
|
|
141
|
+
// Kam waehrend des Abrufs eine Meldung, ist seine Antwort zu „in Arbeit“ womoeglich von davor: sie zaehlt nicht.
|
|
142
|
+
const meldungen = lauf.meldungen
|
|
143
|
+
lauf.gemeldetFuer = zettel.projekt_id
|
|
144
|
+
const antwort = await anApp($, lauf, '/api/mod/lebenszeichen', ausweis.schluessel, { projektId: zettel.projekt_id, sitzung })
|
|
145
|
+
if (vorbei()) return
|
|
146
|
+
|
|
147
|
+
if (antwort?.status === 200) {
|
|
148
|
+
const gelesen = antwortAus(antwort.text)
|
|
149
|
+
// Kann der Mod nicht feststellen, ob es geklappt hat, gilt es als Fehler (US-4).
|
|
150
|
+
if (gelesen === null) return fehlerSind($, lauf, ['nicht-erreichbar'])
|
|
151
|
+
if (lauf.meldungen === meldungen) lauf.inArbeit = gelesen.inArbeit
|
|
152
|
+
lauf.specDateien = gelesen.specDateien
|
|
153
|
+
fehlerSind($, lauf, [])
|
|
154
|
+
if (lauf.meldungenPruefen) {
|
|
155
|
+
lauf.meldungenPruefen = false
|
|
156
|
+
await gemerkteMeldungenAnstossen($, lauf)
|
|
157
|
+
}
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
if (antwort?.status === 401) return fehlerSind($, lauf, ['nicht-angemeldet'])
|
|
161
|
+
// `403`: das Projekt gehoert diesem Konto nicht (mehr) -- etwa ein Kollege mit demselben Repo, oder in der App
|
|
162
|
+
// geloescht (das klaert `kimal3 projekt verbinden`, S-31). Kein Fehler fuer den Menschen, kein weiterer Versuch.
|
|
163
|
+
if (antwort?.status === 403) {
|
|
164
|
+
lauf.abgelehnt = zettel.projekt_id
|
|
165
|
+
lauf.inArbeit = null
|
|
166
|
+
return fehlerSind($, lauf, [])
|
|
167
|
+
}
|
|
168
|
+
// `410`: diese Sitzung ist schon abgemeldet -- nie wiederbeleben, sonst kaeme der Punkt zurueck (Review Focus 5).
|
|
169
|
+
if (antwort?.status === 410) {
|
|
170
|
+
lauf.takt?.cancel()
|
|
171
|
+
lauf.takt = null
|
|
172
|
+
lauf.inArbeit = null
|
|
173
|
+
return fehlerSind($, lauf, [])
|
|
174
|
+
}
|
|
175
|
+
fehlerSind($, lauf, ['nicht-erreichbar'])
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** `{ inArbeit, specDateien }` aus der Antwort; ohne gueltiges `inArbeit` `null`. Fehlt `specDateien` (aeltere App): leer. */
|
|
179
|
+
function antwortAus(text: string): { inArbeit: boolean; specDateien: string[] } | null {
|
|
180
|
+
try {
|
|
181
|
+
const o: unknown = JSON.parse(text)
|
|
182
|
+
const inArbeit = (o as { inArbeit?: unknown } | null)?.inArbeit
|
|
183
|
+
const dateien = (o as { specDateien?: unknown } | null)?.specDateien
|
|
184
|
+
if (typeof inArbeit !== 'boolean') return null
|
|
185
|
+
return { inArbeit, specDateien: Array.isArray(dateien) ? dateien.filter((d): d is string => typeof d === 'string') : [] }
|
|
186
|
+
} catch {
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function abmelden($: EngineInterface, lauf: Lauf, projektId: string, sitzung: string, grenzeMs: number): Promise<void> {
|
|
192
|
+
const ausweis = await liesAusweis($, lauf)
|
|
193
|
+
if ('fehler' in ausweis) return
|
|
194
|
+
// Der Abruf brauchte gemessen 4 ms (M-2). Reicht die Zeit nicht, kostet das hoechstens den Punkt fuer 30 Sekunden.
|
|
195
|
+
const antwort = await anApp($, lauf, '/api/mod/abmelden', ausweis.schluessel, { projektId, sitzung }, grenzeMs)
|
|
196
|
+
if (antwort?.status !== 204) $.ui.log(`kimal3-plan: abmelden ${antwort === null ? 'nicht erreichbar' : antwort.status}`, { to: 'debug' })
|
|
197
|
+
}
|