instar 0.28.87 → 0.28.88
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/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +9 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/core/ProjectDigestCache.d.ts +58 -0
- package/dist/core/ProjectDigestCache.d.ts.map +1 -0
- package/dist/core/ProjectDigestCache.js +146 -0
- package/dist/core/ProjectDigestCache.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +13 -5
- package/src/templates/hooks/compaction-recovery.sh +50 -0
- package/src/templates/hooks/session-start.sh +50 -0
- package/upgrades/0.28.88.md +91 -0
- package/upgrades/side-effects/0.28.87.md +13 -0
- package/upgrades/side-effects/project-scope-phase1a-pr3.md +477 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { InitiativeTracker, Initiative } from './InitiativeTracker.js';
|
|
2
|
+
/** Where the digest lands. Hooks read this exact path. */
|
|
3
|
+
export declare const DIGEST_CACHE_FILENAME = "projects-digest.cache";
|
|
4
|
+
/** Max projects rendered into `digestLines`. Top-N by `lastTouchedAt`. */
|
|
5
|
+
export declare const MAX_PROJECTS_IN_DIGEST = 5;
|
|
6
|
+
/** Max characters per sanitized string (titles, round names). */
|
|
7
|
+
export declare const MAX_STRING_LENGTH = 80;
|
|
8
|
+
/** JSON shape that the hooks deserialize. */
|
|
9
|
+
export interface ProjectDigestCacheFile {
|
|
10
|
+
/** ISO timestamp when this snapshot was written. */
|
|
11
|
+
generatedAt: string;
|
|
12
|
+
/** One sanitized one-liner per active project, top-N by `lastTouchedAt`. */
|
|
13
|
+
digestLines: string[];
|
|
14
|
+
/** Total active projects in the tracker (independent of truncation). */
|
|
15
|
+
totalActiveProjects: number;
|
|
16
|
+
/** True iff `totalActiveProjects > MAX_PROJECTS_IN_DIGEST`. */
|
|
17
|
+
truncated: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Strip control chars (incl. \n, \r, \t), then collapse runs of internal
|
|
21
|
+
* whitespace to single spaces and cap at `cap` chars.
|
|
22
|
+
*
|
|
23
|
+
* Exported so the hooks (well, their JS-side helpers) and tests can apply
|
|
24
|
+
* the same rule on read. The bash hooks themselves use a `tr`-based
|
|
25
|
+
* equivalent — the literal regex below is the canonical implementation.
|
|
26
|
+
*/
|
|
27
|
+
export declare function sanitizeDigestString(input: unknown, cap?: number): string;
|
|
28
|
+
/**
|
|
29
|
+
* Render one digest line for a single project. Pure function — caller
|
|
30
|
+
* provides the project; sanitization happens inside.
|
|
31
|
+
*/
|
|
32
|
+
export declare function formatProjectDigestLine(project: Initiative): string;
|
|
33
|
+
/**
|
|
34
|
+
* ProjectDigestCache — single-responsibility writer. Construct once at
|
|
35
|
+
* server boot; pass `writeDigestCache.bind(this)` as the invalidator on
|
|
36
|
+
* `InitiativeTracker.setDigestCacheInvalidator(fn)`.
|
|
37
|
+
*/
|
|
38
|
+
export declare class ProjectDigestCache {
|
|
39
|
+
private readonly filePath;
|
|
40
|
+
private readonly tracker;
|
|
41
|
+
/** Bound to `process.pid` so concurrent writers (test sandbox + main
|
|
42
|
+
* process) don't trample one another's temp files. */
|
|
43
|
+
private readonly tmpSuffix;
|
|
44
|
+
constructor(stateDir: string, tracker: InitiativeTracker);
|
|
45
|
+
/** Absolute path to the cache file. Exposed for diagnostics. */
|
|
46
|
+
getCachePath(): string;
|
|
47
|
+
/**
|
|
48
|
+
* Snapshot all active projects, render the top-N digest lines, and write
|
|
49
|
+
* the result atomically. Synchronous on purpose — runs in the
|
|
50
|
+
* invalidator hot path (every project mutation calls it once).
|
|
51
|
+
*
|
|
52
|
+
* Best-effort: any thrown error is swallowed after logging, so a hiccup
|
|
53
|
+
* writing the cache never blocks a successful mutation.
|
|
54
|
+
*/
|
|
55
|
+
writeDigestCache(): void;
|
|
56
|
+
private atomicWrite;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=ProjectDigestCache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectDigestCache.d.ts","sourceRoot":"","sources":["../../src/core/ProjectDigestCache.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAE5E,0DAA0D;AAC1D,eAAO,MAAM,qBAAqB,0BAA0B,CAAC;AAE7D,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC,iEAAiE;AACjE,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,6CAA6C;AAC7C,MAAM,WAAW,sBAAsB;IACrC,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,wEAAwE;IACxE,mBAAmB,EAAE,MAAM,CAAC;IAC5B,+DAA+D;IAC/D,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,SAAoB,GAAG,MAAM,CAUpF;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,UAAU,GAAG,MAAM,CAsBnE;AAaD;;;;GAIG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAC5C;2DACuD;IACvD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB;IAMxD,gEAAgE;IAChE,YAAY,IAAI,MAAM;IAItB;;;;;;;OAOG;IACH,gBAAgB,IAAI,IAAI;IA4BxB,OAAO,CAAC,WAAW;CAOpB"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectDigestCache — writes a sanitized JSON snapshot of active projects
|
|
3
|
+
* to `.instar/projects-digest.cache` for cheap, no-HTTP consumption by
|
|
4
|
+
* session-start and compaction-recovery hooks.
|
|
5
|
+
*
|
|
6
|
+
* Spec source: PROJECT-SCOPE-SPEC.md § Phase 1.9 (session-start + compaction
|
|
7
|
+
* recovery hooks). The hooks have a ≤50ms budget — pure file read, no HTTP.
|
|
8
|
+
*
|
|
9
|
+
* Invariants:
|
|
10
|
+
* - Top 5 active projects by `lastTouchedAt` (desc) are included; the
|
|
11
|
+
* `truncated` flag + `totalActiveProjects` count make over-limit visible.
|
|
12
|
+
* - Every string field that lands in the cache is sanitized at write time:
|
|
13
|
+
* control chars (incl. newline + CR) stripped, and the result capped at
|
|
14
|
+
* 80 characters. The hooks ALSO sanitize on read (defense in depth),
|
|
15
|
+
* so direct cache-file poisoning can't smuggle an unprintable string
|
|
16
|
+
* into orientation output.
|
|
17
|
+
* - Atomic on-disk write (temp file + rename) — readers never observe a
|
|
18
|
+
* partial file. Matches the pattern `InitiativeTracker.saveToDisk()`
|
|
19
|
+
* uses for its own persistence.
|
|
20
|
+
*
|
|
21
|
+
* This module is the **read side** of the invalidator hook PR 1 added on
|
|
22
|
+
* InitiativeTracker (`setDigestCacheInvalidator`). The server wires
|
|
23
|
+
* `writeDigestCache()` as the invalidator at boot, so every successful
|
|
24
|
+
* project mutation re-renders the file. Mutations on `kind:'task'`
|
|
25
|
+
* initiatives also trigger a rewrite — the function filters internally to
|
|
26
|
+
* `kind:'project' && status:'active'`, so the side effect is a no-op when
|
|
27
|
+
* no project state changed.
|
|
28
|
+
*/
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
/** Where the digest lands. Hooks read this exact path. */
|
|
32
|
+
export const DIGEST_CACHE_FILENAME = 'projects-digest.cache';
|
|
33
|
+
/** Max projects rendered into `digestLines`. Top-N by `lastTouchedAt`. */
|
|
34
|
+
export const MAX_PROJECTS_IN_DIGEST = 5;
|
|
35
|
+
/** Max characters per sanitized string (titles, round names). */
|
|
36
|
+
export const MAX_STRING_LENGTH = 80;
|
|
37
|
+
/**
|
|
38
|
+
* Strip control chars (incl. \n, \r, \t), then collapse runs of internal
|
|
39
|
+
* whitespace to single spaces and cap at `cap` chars.
|
|
40
|
+
*
|
|
41
|
+
* Exported so the hooks (well, their JS-side helpers) and tests can apply
|
|
42
|
+
* the same rule on read. The bash hooks themselves use a `tr`-based
|
|
43
|
+
* equivalent — the literal regex below is the canonical implementation.
|
|
44
|
+
*/
|
|
45
|
+
export function sanitizeDigestString(input, cap = MAX_STRING_LENGTH) {
|
|
46
|
+
const raw = typeof input === 'string' ? input : input == null ? '' : String(input);
|
|
47
|
+
// Strip ASCII control chars (0x00-0x1F) and DEL (0x7F).
|
|
48
|
+
// Replaces with a single space so words don't collide.
|
|
49
|
+
// eslint-disable-next-line no-control-regex
|
|
50
|
+
const stripped = raw.replace(/[\x00-\x1F\x7F]/g, ' ');
|
|
51
|
+
// Collapse internal whitespace runs, trim ends.
|
|
52
|
+
const collapsed = stripped.replace(/\s+/g, ' ').trim();
|
|
53
|
+
if (collapsed.length <= cap)
|
|
54
|
+
return collapsed;
|
|
55
|
+
return collapsed.slice(0, cap);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Render one digest line for a single project. Pure function — caller
|
|
59
|
+
* provides the project; sanitization happens inside.
|
|
60
|
+
*/
|
|
61
|
+
export function formatProjectDigestLine(project) {
|
|
62
|
+
const id = sanitizeDigestString(project.id, MAX_STRING_LENGTH);
|
|
63
|
+
const rounds = Array.isArray(project.rounds) ? project.rounds : [];
|
|
64
|
+
const total = rounds.length;
|
|
65
|
+
const done = rounds.filter((r) => r && (r.status === 'complete' || r.status === 'complete-with-skips')).length;
|
|
66
|
+
// Next round: first non-complete round in order. Falls through to "(none)"
|
|
67
|
+
// when every round is done.
|
|
68
|
+
const next = rounds.find((r) => r &&
|
|
69
|
+
r.status !== 'complete' &&
|
|
70
|
+
r.status !== 'complete-with-skips' &&
|
|
71
|
+
r.status !== 'failed');
|
|
72
|
+
const nextLabel = next
|
|
73
|
+
? sanitizeDigestString(next.name || '(unnamed round)', MAX_STRING_LENGTH)
|
|
74
|
+
: '(none — all rounds complete)';
|
|
75
|
+
return `Project [${id}]: ${done} of ${total} done. Next round: ${nextLabel}.`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Sort projects by `lastTouchedAt` descending. Ties broken by `id` ascending
|
|
79
|
+
* so the output is stable.
|
|
80
|
+
*/
|
|
81
|
+
function sortByLastTouched(a, b) {
|
|
82
|
+
const at = a.lastTouchedAt ?? '';
|
|
83
|
+
const bt = b.lastTouchedAt ?? '';
|
|
84
|
+
if (at === bt)
|
|
85
|
+
return a.id.localeCompare(b.id);
|
|
86
|
+
return bt.localeCompare(at);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* ProjectDigestCache — single-responsibility writer. Construct once at
|
|
90
|
+
* server boot; pass `writeDigestCache.bind(this)` as the invalidator on
|
|
91
|
+
* `InitiativeTracker.setDigestCacheInvalidator(fn)`.
|
|
92
|
+
*/
|
|
93
|
+
export class ProjectDigestCache {
|
|
94
|
+
filePath;
|
|
95
|
+
tracker;
|
|
96
|
+
/** Bound to `process.pid` so concurrent writers (test sandbox + main
|
|
97
|
+
* process) don't trample one another's temp files. */
|
|
98
|
+
tmpSuffix;
|
|
99
|
+
constructor(stateDir, tracker) {
|
|
100
|
+
this.filePath = path.join(stateDir, DIGEST_CACHE_FILENAME);
|
|
101
|
+
this.tracker = tracker;
|
|
102
|
+
this.tmpSuffix = `.${process.pid}.tmp`;
|
|
103
|
+
}
|
|
104
|
+
/** Absolute path to the cache file. Exposed for diagnostics. */
|
|
105
|
+
getCachePath() {
|
|
106
|
+
return this.filePath;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Snapshot all active projects, render the top-N digest lines, and write
|
|
110
|
+
* the result atomically. Synchronous on purpose — runs in the
|
|
111
|
+
* invalidator hot path (every project mutation calls it once).
|
|
112
|
+
*
|
|
113
|
+
* Best-effort: any thrown error is swallowed after logging, so a hiccup
|
|
114
|
+
* writing the cache never blocks a successful mutation.
|
|
115
|
+
*/
|
|
116
|
+
writeDigestCache() {
|
|
117
|
+
try {
|
|
118
|
+
const all = this.tracker.list({ kind: 'project', status: 'active' });
|
|
119
|
+
// `list()` already sorts by `lastTouchedAt` desc; re-sort defensively
|
|
120
|
+
// in case the contract changes underneath us.
|
|
121
|
+
const sorted = [...all].sort(sortByLastTouched);
|
|
122
|
+
const total = sorted.length;
|
|
123
|
+
const truncated = total > MAX_PROJECTS_IN_DIGEST;
|
|
124
|
+
const top = sorted.slice(0, MAX_PROJECTS_IN_DIGEST);
|
|
125
|
+
const digestLines = top.map((p) => formatProjectDigestLine(p));
|
|
126
|
+
const payload = {
|
|
127
|
+
generatedAt: new Date().toISOString(),
|
|
128
|
+
digestLines,
|
|
129
|
+
totalActiveProjects: total,
|
|
130
|
+
truncated,
|
|
131
|
+
};
|
|
132
|
+
this.atomicWrite(JSON.stringify(payload, null, 2));
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
console.warn(`[ProjectDigestCache] writeDigestCache failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
atomicWrite(contents) {
|
|
139
|
+
const dir = path.dirname(this.filePath);
|
|
140
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
141
|
+
const tmp = `${this.filePath}${this.tmpSuffix}`;
|
|
142
|
+
fs.writeFileSync(tmp, contents);
|
|
143
|
+
fs.renameSync(tmp, this.filePath);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=ProjectDigestCache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectDigestCache.js","sourceRoot":"","sources":["../../src/core/ProjectDigestCache.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-05-
|
|
5
|
-
"instarVersion": "0.28.
|
|
6
|
-
"entryCount":
|
|
4
|
+
"generatedAt": "2026-05-11T23:38:42.167Z",
|
|
5
|
+
"instarVersion": "0.28.88",
|
|
6
|
+
"entryCount": 188,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
9
9
|
"id": "hook:session-start",
|
|
@@ -363,6 +363,14 @@
|
|
|
363
363
|
"contentHash": "5b3557fa3662cf0c7b7bab85a252cc2e567b3fc64a04a92d430374ef1f264ca6",
|
|
364
364
|
"since": "2025-01-01"
|
|
365
365
|
},
|
|
366
|
+
"skill:instar-project": {
|
|
367
|
+
"id": "skill:instar-project",
|
|
368
|
+
"type": "skill",
|
|
369
|
+
"domain": "skills",
|
|
370
|
+
"sourcePath": ".claude/skills/instar-project/SKILL.md",
|
|
371
|
+
"contentHash": "8c8a924740d8afedeb5e0b4bea7a3760c9654dc6b49a9a415429fe0366ff8429",
|
|
372
|
+
"since": "2025-01-01"
|
|
373
|
+
},
|
|
366
374
|
"skill:secret-setup": {
|
|
367
375
|
"id": "skill:secret-setup",
|
|
368
376
|
"type": "skill",
|
|
@@ -984,7 +992,7 @@
|
|
|
984
992
|
"type": "template",
|
|
985
993
|
"domain": "safety",
|
|
986
994
|
"sourcePath": "src/templates/hooks/compaction-recovery.sh",
|
|
987
|
-
"contentHash": "
|
|
995
|
+
"contentHash": "ca6a2a5c2ee03839654c9708912292bc03d81c61281dffbbb958d4877ee1dc91",
|
|
988
996
|
"since": "2025-01-01"
|
|
989
997
|
},
|
|
990
998
|
"template:dangerous-command-guard.sh": {
|
|
@@ -1024,7 +1032,7 @@
|
|
|
1024
1032
|
"type": "template",
|
|
1025
1033
|
"domain": "safety",
|
|
1026
1034
|
"sourcePath": "src/templates/hooks/session-start.sh",
|
|
1027
|
-
"contentHash": "
|
|
1035
|
+
"contentHash": "4d98736d3ee30fe8639f0851e5e492416ad05f4ae49f84e8695723faa4b9398e",
|
|
1028
1036
|
"since": "2025-01-01"
|
|
1029
1037
|
},
|
|
1030
1038
|
"template:settings-template.json": {
|
|
@@ -464,5 +464,55 @@ if [ -f "$CONFIG_FILE" ] && command -v tmux >/dev/null 2>&1; then
|
|
|
464
464
|
fi
|
|
465
465
|
fi
|
|
466
466
|
|
|
467
|
+
# Active projects digest — file-first, no HTTP (PROJECT-SCOPE-SPEC Phase 1.9).
|
|
468
|
+
# Re-injects the active-project orientation after compaction so the agent
|
|
469
|
+
# doesn't lose track of multi-spec work. Same cache as session-start.sh.
|
|
470
|
+
DIGEST_FILE="$INSTAR_DIR/projects-digest.cache"
|
|
471
|
+
echo ""
|
|
472
|
+
if [ -f "$DIGEST_FILE" ]; then
|
|
473
|
+
PROJ_DIGEST=$(python3 - "$DIGEST_FILE" <<'PYEOF' 2>/dev/null
|
|
474
|
+
import json, re, sys
|
|
475
|
+
path = sys.argv[1]
|
|
476
|
+
try:
|
|
477
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
478
|
+
d = json.load(f)
|
|
479
|
+
lines = d.get('digestLines') or []
|
|
480
|
+
if not isinstance(lines, list):
|
|
481
|
+
sys.exit(0)
|
|
482
|
+
ctrl = re.compile(r'[\x00-\x1F\x7F]')
|
|
483
|
+
cleaned = []
|
|
484
|
+
for ln in lines[:5]:
|
|
485
|
+
if not isinstance(ln, str):
|
|
486
|
+
continue
|
|
487
|
+
s = ctrl.sub(' ', ln)
|
|
488
|
+
s = re.sub(r'\s+', ' ', s).strip()
|
|
489
|
+
if not s:
|
|
490
|
+
continue
|
|
491
|
+
cleaned.append(s[:200])
|
|
492
|
+
if not cleaned:
|
|
493
|
+
print('Active projects: none')
|
|
494
|
+
else:
|
|
495
|
+
print('--- ACTIVE PROJECTS (post-compaction) ---')
|
|
496
|
+
for c in cleaned:
|
|
497
|
+
print(c)
|
|
498
|
+
truncated = bool(d.get('truncated'))
|
|
499
|
+
total = d.get('totalActiveProjects')
|
|
500
|
+
if truncated and isinstance(total, int) and total > len(cleaned):
|
|
501
|
+
more = total - len(cleaned)
|
|
502
|
+
print(f'+{more} more on dashboard.')
|
|
503
|
+
print('--- END ACTIVE PROJECTS ---')
|
|
504
|
+
except Exception:
|
|
505
|
+
sys.exit(0)
|
|
506
|
+
PYEOF
|
|
507
|
+
)
|
|
508
|
+
if [ -n "$PROJ_DIGEST" ]; then
|
|
509
|
+
echo "$PROJ_DIGEST"
|
|
510
|
+
else
|
|
511
|
+
echo "Active projects: state unavailable — run /project status when ready"
|
|
512
|
+
fi
|
|
513
|
+
else
|
|
514
|
+
echo "Active projects: state unavailable — run /project status when ready"
|
|
515
|
+
fi
|
|
516
|
+
|
|
467
517
|
echo ""
|
|
468
518
|
echo "=== RECOVERY COMPLETE — You are grounded. Continue your work. ==="
|
|
@@ -355,4 +355,54 @@ except Exception:
|
|
|
355
355
|
fi
|
|
356
356
|
fi
|
|
357
357
|
|
|
358
|
+
# Active projects digest — file-first, no HTTP (PROJECT-SCOPE-SPEC Phase 1.9).
|
|
359
|
+
# Reads .instar/projects-digest.cache, written by the server on every
|
|
360
|
+
# project mutation. Re-sanitizes each line on read for defense in depth.
|
|
361
|
+
DIGEST_FILE="$INSTAR_DIR/projects-digest.cache"
|
|
362
|
+
echo ""
|
|
363
|
+
if [ -f "$DIGEST_FILE" ]; then
|
|
364
|
+
PROJ_DIGEST=$(python3 - "$DIGEST_FILE" <<'PYEOF' 2>/dev/null
|
|
365
|
+
import json, re, sys
|
|
366
|
+
path = sys.argv[1]
|
|
367
|
+
try:
|
|
368
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
369
|
+
d = json.load(f)
|
|
370
|
+
lines = d.get('digestLines') or []
|
|
371
|
+
if not isinstance(lines, list):
|
|
372
|
+
sys.exit(0)
|
|
373
|
+
ctrl = re.compile(r'[\x00-\x1F\x7F]')
|
|
374
|
+
cleaned = []
|
|
375
|
+
for ln in lines[:5]:
|
|
376
|
+
if not isinstance(ln, str):
|
|
377
|
+
continue
|
|
378
|
+
s = ctrl.sub(' ', ln)
|
|
379
|
+
s = re.sub(r'\s+', ' ', s).strip()
|
|
380
|
+
if not s:
|
|
381
|
+
continue
|
|
382
|
+
cleaned.append(s[:200])
|
|
383
|
+
if not cleaned:
|
|
384
|
+
print('Active projects: none')
|
|
385
|
+
else:
|
|
386
|
+
print('--- ACTIVE PROJECTS ---')
|
|
387
|
+
for c in cleaned:
|
|
388
|
+
print(c)
|
|
389
|
+
truncated = bool(d.get('truncated'))
|
|
390
|
+
total = d.get('totalActiveProjects')
|
|
391
|
+
if truncated and isinstance(total, int) and total > len(cleaned):
|
|
392
|
+
more = total - len(cleaned)
|
|
393
|
+
print(f'+{more} more on dashboard.')
|
|
394
|
+
print('--- END ACTIVE PROJECTS ---')
|
|
395
|
+
except Exception:
|
|
396
|
+
sys.exit(0)
|
|
397
|
+
PYEOF
|
|
398
|
+
)
|
|
399
|
+
if [ -n "$PROJ_DIGEST" ]; then
|
|
400
|
+
echo "$PROJ_DIGEST"
|
|
401
|
+
else
|
|
402
|
+
echo "Active projects: state unavailable — run /project status when ready"
|
|
403
|
+
fi
|
|
404
|
+
else
|
|
405
|
+
echo "Active projects: state unavailable — run /project status when ready"
|
|
406
|
+
fi
|
|
407
|
+
|
|
358
408
|
exit 0
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: minor -->
|
|
4
|
+
<!-- Valid values: patch, minor, major -->
|
|
5
|
+
<!-- patch = bug fixes, refactors, test additions, doc updates -->
|
|
6
|
+
<!-- minor = new features, new APIs, new capabilities (backwards-compatible) -->
|
|
7
|
+
<!-- major = breaking changes to existing APIs or behavior -->
|
|
8
|
+
|
|
9
|
+
## What Changed
|
|
10
|
+
|
|
11
|
+
### Project-scope Phase 1a PR 3 — `/project` skill + session-start digest
|
|
12
|
+
|
|
13
|
+
Final of three PRs scaffolding the project-scope feature. PR 1 added
|
|
14
|
+
the type-level fields; PR 2 added the stage-transition validator and
|
|
15
|
+
HTTP surface; PR 3 ships the user-invocable surface and the
|
|
16
|
+
session-start orientation block so active projects stay visible across
|
|
17
|
+
new sessions.
|
|
18
|
+
|
|
19
|
+
Spec source: `docs/specs/PROJECT-SCOPE-SPEC.md` §§ Phase 1.7 (skill),
|
|
20
|
+
Phase 1.9 (session-start + compaction-recovery hooks).
|
|
21
|
+
|
|
22
|
+
**New files:**
|
|
23
|
+
- `src/core/ProjectDigestCache.ts` — synchronous writer that
|
|
24
|
+
snapshots active projects to `.instar/projects-digest.cache`. Top 5
|
|
25
|
+
by `lastTouchedAt`, sanitized strings (control-char strip + 80-char
|
|
26
|
+
per-field cap), atomic write.
|
|
27
|
+
- `.claude/skills/instar-project/SKILL.md` — slash-command skill. Phase
|
|
28
|
+
1a ships `/project create`, `/project status`, and `/project next`
|
|
29
|
+
(read-only + create). The mutating commands ship in Phase 1b along
|
|
30
|
+
with the round-runner.
|
|
31
|
+
|
|
32
|
+
**Wired-in behavior:**
|
|
33
|
+
- Server boot now constructs a `ProjectDigestCache` and registers it
|
|
34
|
+
as the digest invalidator on `InitiativeTracker`. Every successful
|
|
35
|
+
project mutation (create / update / status change) re-renders the
|
|
36
|
+
cache file atomically. First boot writes the cache unconditionally
|
|
37
|
+
so the hook scripts always have a well-formed file to read.
|
|
38
|
+
- `.instar/hooks/instar/session-start.sh` and
|
|
39
|
+
`.instar/hooks/instar/compaction-recovery.sh` append an
|
|
40
|
+
`--- ACTIVE PROJECTS ---` block at the end of their orientation
|
|
41
|
+
output. They read the cache file (no HTTP, ≤50ms budget), re-sanitize
|
|
42
|
+
each line on read (defense in depth), and fall back to
|
|
43
|
+
`Active projects: state unavailable — run /project status when ready`
|
|
44
|
+
on miss / parse error.
|
|
45
|
+
|
|
46
|
+
**Cache file shape (`.instar/projects-digest.cache`):**
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"generatedAt": "2026-05-11T...",
|
|
50
|
+
"digestLines": [
|
|
51
|
+
"Project [my-project]: 2 of 5 done. Next round: Round 3."
|
|
52
|
+
],
|
|
53
|
+
"totalActiveProjects": 1,
|
|
54
|
+
"truncated": false
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## What to Tell Your User
|
|
59
|
+
|
|
60
|
+
PR 3 closes the first slice of the project-tracking work. Your agent now has a built-in slash command for inspecting and registering projects, plus a session-start orientation line that keeps active projects visible at the top of every new conversation.
|
|
61
|
+
|
|
62
|
+
What you'll see at session start: a short Active Projects block with up to five projects, one line each — telling you the project name, how many rounds are done out of the total, and what the next round is. If you have more than five active projects, the block ends with a note saying how many more are on the dashboard. If your agent has no projects registered yet, the block is empty.
|
|
63
|
+
|
|
64
|
+
What this changes day-to-day: your agent won't lose track of multi-spec projects after a session restart or a context compaction. Before this, the first few features in a project would get attention and the rest would drift off the radar. Now the project roster is structurally visible at every new conversation. The same digest re-injects after context compression, so a long session that hits the compaction window still keeps the active-project orientation in context.
|
|
65
|
+
|
|
66
|
+
The project skill itself is this slice's read-only surface: you can register a project from a markdown plan doc, list all projects, fetch one with its children, or ask "what's next?" (which currently returns a placeholder — the next phase will wire the real answer). The mutating commands that actually drive a project forward — advancing rounds, halting, acknowledging, resuming, abandoning, running a round, drift-checking, accepting a partial round, and claiming ownership across machines — ship in the next phase once the round-runner lands.
|
|
67
|
+
|
|
68
|
+
Nothing existing changes shape. The initiatives routes still return what they always did. The new session-start block appears at the end of the orientation output and is plain text. If your agent doesn't register projects, you won't see any difference.
|
|
69
|
+
|
|
70
|
+
## Summary of New Capabilities
|
|
71
|
+
|
|
72
|
+
| Capability | How to Use |
|
|
73
|
+
|-----------|-----------|
|
|
74
|
+
| `/project create <plan-doc>` | Register a project from a markdown plan via `POST /projects` |
|
|
75
|
+
| `/project status [id]` | List all projects, or fetch one with its children, via `GET /projects[/:id]` |
|
|
76
|
+
| `/project next [id]` | Phase 1a placeholder; returns the spec's 501 response |
|
|
77
|
+
| Session-start project digest | Top 5 active projects rendered at every new session start (no extra config) |
|
|
78
|
+
| Compaction-recovery digest | Same digest re-injects after context compression |
|
|
79
|
+
| Cache file (read-only consumer) | `.instar/projects-digest.cache` — JSON snapshot, atomic writes, sanitized |
|
|
80
|
+
|
|
81
|
+
## Evidence
|
|
82
|
+
|
|
83
|
+
Spec: `docs/specs/PROJECT-SCOPE-SPEC.md` §§ 1.7, 1.9.
|
|
84
|
+
|
|
85
|
+
- `tests/unit/ProjectDigestCache.test.ts` — 19 new tests covering
|
|
86
|
+
empty case, 3-project + 7-project cases, sanitization (control char
|
|
87
|
+
strip + 80-char cap), atomic write, invalidator wiring.
|
|
88
|
+
- `tests/unit/project-digest-hooks.test.ts` — 7 new tests exercising
|
|
89
|
+
the bash hook scripts against present / missing / malformed /
|
|
90
|
+
poisoned cache files.
|
|
91
|
+
- Side-effects review: `upgrades/side-effects/project-scope-phase1a-pr3.md`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Side-Effects Review — v0.28.87
|
|
2
|
+
|
|
3
|
+
This release shipped two user-facing changes:
|
|
4
|
+
|
|
5
|
+
- **Stuck-Telegram-message auto-recovery (PR #151)** — side-effects review at
|
|
6
|
+
[`verify-injection-stuck-input.md`](./verify-injection-stuck-input.md).
|
|
7
|
+
- **Project-scope Phase 1a PR 2 — stage validator + `/projects` API** —
|
|
8
|
+
side-effects review at
|
|
9
|
+
[`project-scope-phase1a-pr2.md`](./project-scope-phase1a-pr2.md).
|
|
10
|
+
|
|
11
|
+
This file is the version-keyed pointer the pre-push gate looks for; the
|
|
12
|
+
per-change artifacts above contain the actual side-effects analysis. No
|
|
13
|
+
other behavior changed in 0.28.87 beyond what those two reviews cover.
|