portable-agent-layer 0.61.1 → 0.61.2

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` | Verifiable done conditions — testable ISCs (Ideal State 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` | Summary of completed milestones |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.61.1",
3
+ "version": "0.61.2",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -351,17 +351,38 @@ function parseIscs(criteria: string): Isc[] {
351
351
  return out;
352
352
  }
353
353
 
354
- function nextIscId(criteria: string): number {
355
- const ids = parseIscs(criteria).map((i) => i.id);
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 patchIsc(criteria: string, id: number, checked: boolean): string {
360
- const marker = checked ? "[x]" : "[ ]";
361
- return criteria.replace(
362
- new RegExp(String.raw`^(-\s+)\[[ x]\](\s+ISC-${id}:)`, "m"),
363
- `$1${marker}$2`
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
+ function archiveLine(changelog: string | undefined, doneLine: string): string {
382
+ const heading = `### Archived ${new Date().toISOString().slice(0, 10)}`;
383
+ const base = (changelog ?? "").trim();
384
+ if (base.includes(heading)) return `${base}\n${doneLine}`;
385
+ return base ? `${base}\n\n${heading}\n${doneLine}` : `${heading}\n${doneLine}`;
365
386
  }
366
387
 
367
388
  function cmdAddIsc(args: string[]): void {
@@ -370,7 +391,7 @@ function cmdAddIsc(args: string[]): void {
370
391
  if (!title) fail("Usage: add-isc <name> <title>");
371
392
  const p = requireProject(name);
372
393
  const current = p.criteria ?? "";
373
- const id = nextIscId(current);
394
+ const id = nextIscId(p);
374
395
  const newLine = `- [ ] ISC-${id}: ${title}`;
375
396
  p.criteria = current ? `${current.trimEnd()}\n${newLine}` : newLine;
376
397
  p.updated = now();
@@ -385,46 +406,56 @@ function cmdAddIsc(args: string[]): void {
385
406
  });
386
407
  }
387
408
 
409
+ // Completing an ISC moves its line out of Criteria and into the dated Changelog
410
+ // archive, so Criteria stays exactly the open set and never re-bloats context.
388
411
  function cmdCompleteIsc(args: string[]): void {
389
412
  const name = args[0] ?? fail("Usage: complete-isc <name> <id>");
390
413
  const id = Number(args[1] ?? fail("Usage: complete-isc <name> <id>"));
391
414
  if (!Number.isInteger(id) || id < 1) fail("ISC id must be a positive integer");
392
415
  const p = requireProject(name);
393
- const current = p.criteria ?? "";
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) {
416
+ if (parseIscs(p.changelog ?? "").some((i) => i.id === id)) {
397
417
  ok({ checked: true, id, alreadyDone: true });
398
418
  return;
399
419
  }
400
- p.criteria = patchIsc(current, id, true);
420
+ const { line, rest } = removeIscLine(p.criteria ?? "", id);
421
+ if (!line) fail(`ISC-${id} not found in project "${name}"`);
422
+ p.criteria = rest;
423
+ p.changelog = archiveLine(p.changelog, line.replace("[ ]", "[x]"));
401
424
  p.updated = now();
402
425
  writeProject(p);
403
- ok({ checked: true, id });
426
+ ok({ checked: true, id, archived: true });
404
427
  }
405
428
 
429
+ // Reopening pulls the line back out of the Changelog (or legacy Criteria) into
430
+ // the open set.
406
431
  function cmdReopenIsc(args: string[]): void {
407
432
  const name = args[0] ?? fail("Usage: reopen-isc <name> <id>");
408
433
  const id = Number(args[1] ?? fail("Usage: reopen-isc <name> <id>"));
409
434
  if (!Number.isInteger(id) || id < 1) fail("ISC id must be a positive integer");
410
435
  const p = requireProject(name);
411
- const current = p.criteria ?? "";
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) {
436
+ if (parseIscs(p.criteria ?? "").some((i) => i.id === id && !i.checked)) {
415
437
  ok({ checked: false, id, alreadyOpen: true });
416
438
  return;
417
439
  }
418
- p.criteria = patchIsc(current, id, false);
440
+ let removed = removeIscLine(p.changelog ?? "", id);
441
+ if (removed.line) {
442
+ p.changelog = removed.rest;
443
+ } else {
444
+ removed = removeIscLine(p.criteria ?? "", id);
445
+ if (removed.line) p.criteria = removed.rest;
446
+ }
447
+ if (!removed.line) fail(`ISC-${id} not found in project "${name}"`);
448
+ const openLine = removed.line.replace(/\[x\]/i, "[ ]");
449
+ p.criteria = p.criteria ? `${p.criteria.trimEnd()}\n${openLine}` : openLine;
419
450
  p.updated = now();
420
451
  writeProject(p);
421
452
  ok({ checked: false, id });
422
453
  }
423
454
 
424
- function selectIscs(all: Isc[], flags: Set<string>): Isc[] {
425
- if (flags.has("--all")) return all;
426
- if (flags.has("--closed")) return all.filter((i) => i.checked);
427
- return all.filter((i) => !i.checked);
455
+ function selectIscs(open: Isc[], done: Isc[], flags: Set<string>): Isc[] {
456
+ if (flags.has("--all")) return [...open, ...done];
457
+ if (flags.has("--closed")) return done;
458
+ return open;
428
459
  }
429
460
 
430
461
  function cmdListIsc(args: string[]): void {
@@ -433,17 +464,38 @@ function cmdListIsc(args: string[]): void {
433
464
  args.find((a) => !a.startsWith("--")) ??
434
465
  fail("Usage: list-isc <name> [--all | --closed]");
435
466
  const p = requireProject(name);
436
- const all = parseIscs(p.criteria ?? "");
437
- const open = all.filter((i) => !i.checked).length;
467
+ const criteria = parseIscs(p.criteria ?? "");
468
+ const open = criteria.filter((i) => !i.checked);
469
+ const done = [...criteria.filter((i) => i.checked), ...parseIscs(p.changelog ?? "")];
438
470
  ok({
439
471
  name,
440
- total: all.length,
441
- open,
442
- done: all.length - open,
443
- iscs: selectIscs(all, flags),
472
+ total: open.length + done.length,
473
+ open: open.length,
474
+ done: done.length,
475
+ iscs: selectIscs(open, done, flags),
444
476
  });
445
477
  }
446
478
 
479
+ // Backfill: sweep any done ISCs still sitting in Criteria (legacy projects, or
480
+ // completions from before archive-on-complete) into the Changelog in one pass.
481
+ function cmdPruneIsc(args: string[]): void {
482
+ const name = args[0] ?? fail("Usage: prune-isc <name>");
483
+ const p = requireProject(name);
484
+ const done = parseIscs(p.criteria ?? "").filter((i) => i.checked);
485
+ for (const isc of done) {
486
+ const { line, rest } = removeIscLine(p.criteria ?? "", isc.id);
487
+ if (!line) continue;
488
+ p.criteria = rest;
489
+ p.changelog = archiveLine(p.changelog, line);
490
+ }
491
+ if (done.length > 0) {
492
+ p.updated = now();
493
+ writeProject(p);
494
+ }
495
+ const openLeft = parseIscs(p.criteria ?? "").filter((i) => !i.checked).length;
496
+ ok({ pruned: done.length, name, remaining_open: openLeft });
497
+ }
498
+
447
499
  // ── Task ISA (work/) ──────────────────────────────────────────────
448
500
 
449
501
  function taskSlug(title: string): string {
@@ -524,6 +576,7 @@ Commands:
524
576
  complete-isc <name> <id> mark ISC-N as done
525
577
  reopen-isc <name> <id> reopen ISC-N (mark not done)
526
578
  list-isc <name> [--all | --closed] list open ISCs (default); --all or --closed for done
579
+ prune-isc <name> archive done ISCs from Criteria into the Changelog
527
580
  isa-init <name> mark project as ISA-initialized
528
581
  scaffold-task-isa <title> create a one-shot task ISA in memory/work/
529
582
  complete-task-isa <slug> mark a task ISA as complete
@@ -615,6 +668,9 @@ function run(): void {
615
668
  case "list-isc":
616
669
  cmdListIsc(rest);
617
670
  return;
671
+ case "prune-isc":
672
+ cmdPruneIsc(rest);
673
+ return;
618
674
  case "isa-init":
619
675
  cmdIsaInit(rest);
620
676
  return;