portable-agent-layer 0.61.1 → 0.61.3
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.
|
@@ -40,13 +40,15 @@ The body of each ISA.md holds spec sections. Use `update-section` to set them:
|
|
|
40
40
|
|---------|-----|---------------|
|
|
41
41
|
| Problem | `problem` | Why this project exists; the pain or gap being addressed |
|
|
42
42
|
| Goal | `goal` | What success looks like (may be bullet list) |
|
|
43
|
-
| Criteria | `criteria` |
|
|
43
|
+
| Criteria | `criteria` | Open ISCs (Ideal State Criteria) — testable done conditions. Holds only the open set; completing an ISC moves it to the Changelog |
|
|
44
44
|
| Vision | `vision` | Long-horizon aspiration beyond the immediate goal |
|
|
45
45
|
| Constraints | `constraints` | Non-negotiable limits (budget, time, tech, compatibility) |
|
|
46
46
|
| Out of Scope | `out_of_scope` | What this project explicitly does NOT cover |
|
|
47
47
|
| Context | `context` | Stable facts / references (e.g. "reference impl lives at ~/pai") |
|
|
48
48
|
| Decisions | `decisions` | Auto-managed by `add-decision`; dated bullet list |
|
|
49
|
-
| Changelog | `changelog` |
|
|
49
|
+
| Changelog | `changelog` | Archive of completed ISCs (`complete-isc` moves them here under a dated `### Archived` heading) plus any milestone notes |
|
|
50
|
+
|
|
51
|
+
**ISC archive model.** `complete-isc` does not just check a box — it moves the ISC line out of Criteria and into the Changelog archive, so Criteria always reflects exactly the open work and never bloats the context loaded at session start. `list-isc <name>` shows open ISCs by default; pass `--closed` (archived only) or `--all` (open + archived) to read finished ones. `reopen-isc` pulls an archived ISC back into the open set. `prune-isc <name>` backfills legacy projects by sweeping any done ISCs still sitting in Criteria into the archive in one pass.
|
|
50
52
|
|
|
51
53
|
## Routing
|
|
52
54
|
|
package/package.json
CHANGED
|
@@ -351,17 +351,55 @@ function parseIscs(criteria: string): Isc[] {
|
|
|
351
351
|
return out;
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
-
|
|
355
|
-
|
|
354
|
+
// Scans Criteria AND Changelog so an archived id can never be handed out again.
|
|
355
|
+
function nextIscId(p: ProjectProgress): number {
|
|
356
|
+
const ids = [...parseIscs(p.criteria ?? ""), ...parseIscs(p.changelog ?? "")].map(
|
|
357
|
+
(i) => i.id
|
|
358
|
+
);
|
|
356
359
|
return ids.length > 0 ? Math.max(...ids) + 1 : 1;
|
|
357
360
|
}
|
|
358
361
|
|
|
359
|
-
function
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
362
|
+
function removeIscLine(
|
|
363
|
+
section: string,
|
|
364
|
+
id: number
|
|
365
|
+
): { line: string | null; rest: string } {
|
|
366
|
+
const lines = section.split("\n");
|
|
367
|
+
const idx = lines.findIndex((l) =>
|
|
368
|
+
new RegExp(String.raw`^-\s+\[[ x]\]\s+ISC-${id}:`).test(l)
|
|
364
369
|
);
|
|
370
|
+
if (idx === -1) return { line: null, rest: section };
|
|
371
|
+
const [line] = lines.splice(idx, 1);
|
|
372
|
+
return {
|
|
373
|
+
line,
|
|
374
|
+
rest: lines
|
|
375
|
+
.join("\n")
|
|
376
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
377
|
+
.trim(),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Drops any "### Archived <date>" heading whose block has no content left —
|
|
382
|
+
// e.g. after every ISC filed under that date has been reopened.
|
|
383
|
+
function dropEmptyArchiveHeadings(changelog: string): string {
|
|
384
|
+
const lines = changelog.split("\n");
|
|
385
|
+
const blockHasContent = (headingIdx: number): boolean => {
|
|
386
|
+
for (let j = headingIdx + 1; j < lines.length && !lines[j].startsWith("### "); j++) {
|
|
387
|
+
if (lines[j].trim() !== "") return true;
|
|
388
|
+
}
|
|
389
|
+
return false;
|
|
390
|
+
};
|
|
391
|
+
return lines
|
|
392
|
+
.filter((l, i) => !(/^### Archived /.test(l) && !blockHasContent(i)))
|
|
393
|
+
.join("\n")
|
|
394
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
395
|
+
.trim();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function archiveLine(changelog: string | undefined, doneLine: string): string {
|
|
399
|
+
const heading = `### Archived ${new Date().toISOString().slice(0, 10)}`;
|
|
400
|
+
const base = (changelog ?? "").trim();
|
|
401
|
+
if (base.includes(heading)) return `${base}\n${doneLine}`;
|
|
402
|
+
return base ? `${base}\n\n${heading}\n${doneLine}` : `${heading}\n${doneLine}`;
|
|
365
403
|
}
|
|
366
404
|
|
|
367
405
|
function cmdAddIsc(args: string[]): void {
|
|
@@ -370,7 +408,7 @@ function cmdAddIsc(args: string[]): void {
|
|
|
370
408
|
if (!title) fail("Usage: add-isc <name> <title>");
|
|
371
409
|
const p = requireProject(name);
|
|
372
410
|
const current = p.criteria ?? "";
|
|
373
|
-
const id = nextIscId(
|
|
411
|
+
const id = nextIscId(p);
|
|
374
412
|
const newLine = `- [ ] ISC-${id}: ${title}`;
|
|
375
413
|
p.criteria = current ? `${current.trimEnd()}\n${newLine}` : newLine;
|
|
376
414
|
p.updated = now();
|
|
@@ -385,46 +423,56 @@ function cmdAddIsc(args: string[]): void {
|
|
|
385
423
|
});
|
|
386
424
|
}
|
|
387
425
|
|
|
426
|
+
// Completing an ISC moves its line out of Criteria and into the dated Changelog
|
|
427
|
+
// archive, so Criteria stays exactly the open set and never re-bloats context.
|
|
388
428
|
function cmdCompleteIsc(args: string[]): void {
|
|
389
429
|
const name = args[0] ?? fail("Usage: complete-isc <name> <id>");
|
|
390
430
|
const id = Number(args[1] ?? fail("Usage: complete-isc <name> <id>"));
|
|
391
431
|
if (!Number.isInteger(id) || id < 1) fail("ISC id must be a positive integer");
|
|
392
432
|
const p = requireProject(name);
|
|
393
|
-
|
|
394
|
-
const existing = parseIscs(current).find((i) => i.id === id);
|
|
395
|
-
if (!existing) fail(`ISC-${id} not found in project "${name}"`);
|
|
396
|
-
if (existing.checked) {
|
|
433
|
+
if (parseIscs(p.changelog ?? "").some((i) => i.id === id)) {
|
|
397
434
|
ok({ checked: true, id, alreadyDone: true });
|
|
398
435
|
return;
|
|
399
436
|
}
|
|
400
|
-
p.criteria
|
|
437
|
+
const { line, rest } = removeIscLine(p.criteria ?? "", id);
|
|
438
|
+
if (!line) fail(`ISC-${id} not found in project "${name}"`);
|
|
439
|
+
p.criteria = rest;
|
|
440
|
+
p.changelog = archiveLine(p.changelog, line.replace("[ ]", "[x]"));
|
|
401
441
|
p.updated = now();
|
|
402
442
|
writeProject(p);
|
|
403
|
-
ok({ checked: true, id });
|
|
443
|
+
ok({ checked: true, id, archived: true });
|
|
404
444
|
}
|
|
405
445
|
|
|
446
|
+
// Reopening pulls the line back out of the Changelog (or legacy Criteria) into
|
|
447
|
+
// the open set.
|
|
406
448
|
function cmdReopenIsc(args: string[]): void {
|
|
407
449
|
const name = args[0] ?? fail("Usage: reopen-isc <name> <id>");
|
|
408
450
|
const id = Number(args[1] ?? fail("Usage: reopen-isc <name> <id>"));
|
|
409
451
|
if (!Number.isInteger(id) || id < 1) fail("ISC id must be a positive integer");
|
|
410
452
|
const p = requireProject(name);
|
|
411
|
-
|
|
412
|
-
const existing = parseIscs(current).find((i) => i.id === id);
|
|
413
|
-
if (!existing) fail(`ISC-${id} not found in project "${name}"`);
|
|
414
|
-
if (!existing.checked) {
|
|
453
|
+
if (parseIscs(p.criteria ?? "").some((i) => i.id === id && !i.checked)) {
|
|
415
454
|
ok({ checked: false, id, alreadyOpen: true });
|
|
416
455
|
return;
|
|
417
456
|
}
|
|
418
|
-
p.
|
|
457
|
+
let removed = removeIscLine(p.changelog ?? "", id);
|
|
458
|
+
if (removed.line) {
|
|
459
|
+
p.changelog = dropEmptyArchiveHeadings(removed.rest);
|
|
460
|
+
} else {
|
|
461
|
+
removed = removeIscLine(p.criteria ?? "", id);
|
|
462
|
+
if (removed.line) p.criteria = removed.rest;
|
|
463
|
+
}
|
|
464
|
+
if (!removed.line) fail(`ISC-${id} not found in project "${name}"`);
|
|
465
|
+
const openLine = removed.line.replace(/\[x\]/i, "[ ]");
|
|
466
|
+
p.criteria = p.criteria ? `${p.criteria.trimEnd()}\n${openLine}` : openLine;
|
|
419
467
|
p.updated = now();
|
|
420
468
|
writeProject(p);
|
|
421
469
|
ok({ checked: false, id });
|
|
422
470
|
}
|
|
423
471
|
|
|
424
|
-
function selectIscs(
|
|
425
|
-
if (flags.has("--all")) return
|
|
426
|
-
if (flags.has("--closed")) return
|
|
427
|
-
return
|
|
472
|
+
function selectIscs(open: Isc[], done: Isc[], flags: Set<string>): Isc[] {
|
|
473
|
+
if (flags.has("--all")) return [...open, ...done];
|
|
474
|
+
if (flags.has("--closed")) return done;
|
|
475
|
+
return open;
|
|
428
476
|
}
|
|
429
477
|
|
|
430
478
|
function cmdListIsc(args: string[]): void {
|
|
@@ -433,17 +481,38 @@ function cmdListIsc(args: string[]): void {
|
|
|
433
481
|
args.find((a) => !a.startsWith("--")) ??
|
|
434
482
|
fail("Usage: list-isc <name> [--all | --closed]");
|
|
435
483
|
const p = requireProject(name);
|
|
436
|
-
const
|
|
437
|
-
const open =
|
|
484
|
+
const criteria = parseIscs(p.criteria ?? "");
|
|
485
|
+
const open = criteria.filter((i) => !i.checked);
|
|
486
|
+
const done = [...criteria.filter((i) => i.checked), ...parseIscs(p.changelog ?? "")];
|
|
438
487
|
ok({
|
|
439
488
|
name,
|
|
440
|
-
total:
|
|
441
|
-
open,
|
|
442
|
-
done:
|
|
443
|
-
iscs: selectIscs(
|
|
489
|
+
total: open.length + done.length,
|
|
490
|
+
open: open.length,
|
|
491
|
+
done: done.length,
|
|
492
|
+
iscs: selectIscs(open, done, flags),
|
|
444
493
|
});
|
|
445
494
|
}
|
|
446
495
|
|
|
496
|
+
// Backfill: sweep any done ISCs still sitting in Criteria (legacy projects, or
|
|
497
|
+
// completions from before archive-on-complete) into the Changelog in one pass.
|
|
498
|
+
function cmdPruneIsc(args: string[]): void {
|
|
499
|
+
const name = args[0] ?? fail("Usage: prune-isc <name>");
|
|
500
|
+
const p = requireProject(name);
|
|
501
|
+
const done = parseIscs(p.criteria ?? "").filter((i) => i.checked);
|
|
502
|
+
for (const isc of done) {
|
|
503
|
+
const { line, rest } = removeIscLine(p.criteria ?? "", isc.id);
|
|
504
|
+
if (!line) continue;
|
|
505
|
+
p.criteria = rest;
|
|
506
|
+
p.changelog = archiveLine(p.changelog, line);
|
|
507
|
+
}
|
|
508
|
+
if (done.length > 0) {
|
|
509
|
+
p.updated = now();
|
|
510
|
+
writeProject(p);
|
|
511
|
+
}
|
|
512
|
+
const openLeft = parseIscs(p.criteria ?? "").filter((i) => !i.checked).length;
|
|
513
|
+
ok({ pruned: done.length, name, remaining_open: openLeft });
|
|
514
|
+
}
|
|
515
|
+
|
|
447
516
|
// ── Task ISA (work/) ──────────────────────────────────────────────
|
|
448
517
|
|
|
449
518
|
function taskSlug(title: string): string {
|
|
@@ -524,6 +593,7 @@ Commands:
|
|
|
524
593
|
complete-isc <name> <id> mark ISC-N as done
|
|
525
594
|
reopen-isc <name> <id> reopen ISC-N (mark not done)
|
|
526
595
|
list-isc <name> [--all | --closed] list open ISCs (default); --all or --closed for done
|
|
596
|
+
prune-isc <name> archive done ISCs from Criteria into the Changelog
|
|
527
597
|
isa-init <name> mark project as ISA-initialized
|
|
528
598
|
scaffold-task-isa <title> create a one-shot task ISA in memory/work/
|
|
529
599
|
complete-task-isa <slug> mark a task ISA as complete
|
|
@@ -615,6 +685,9 @@ function run(): void {
|
|
|
615
685
|
case "list-isc":
|
|
616
686
|
cmdListIsc(rest);
|
|
617
687
|
return;
|
|
688
|
+
case "prune-isc":
|
|
689
|
+
cmdPruneIsc(rest);
|
|
690
|
+
return;
|
|
618
691
|
case "isa-init":
|
|
619
692
|
cmdIsaInit(rest);
|
|
620
693
|
return;
|