@appchy/jarvis 0.1.112 → 0.1.114
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/bin.js +1 -1
- package/harness/harness/align.py +9 -6
- package/harness/harness/epic.py +84 -56
- package/harness/harness/shift.py +4 -3
- package/harness/harness/version.py +5 -5
- package/harness/presets/appchy/PRESET.md +12 -9
- package/harness/test_work.py +58 -31
- package/package.json +6 -6
package/dist/bin.js
CHANGED
|
@@ -10284,7 +10284,7 @@ import { createRequire as createRequire2 } from "module";
|
|
|
10284
10284
|
var _require = createRequire2(import.meta.url);
|
|
10285
10285
|
var VERSION2 = _require("../package.json").version ?? "0.0.0";
|
|
10286
10286
|
var IS_DEV = String(_require("../package.json").name ?? "").endsWith("-dev");
|
|
10287
|
-
var SHA = "
|
|
10287
|
+
var SHA = "3c9f85d";
|
|
10288
10288
|
var BUILT = "2026-09-12";
|
|
10289
10289
|
var BUILD = SHA ?? "source";
|
|
10290
10290
|
var BUILD_LABEL = `${SHA ? `${VERSION2} (${SHA}${BUILT ? ` ${BUILT}` : ""})` : `${VERSION2} (source)`}${IS_DEV ? " \u2014 development build" : ""}`;
|
package/harness/harness/align.py
CHANGED
|
@@ -400,14 +400,17 @@ def _align_versions(root: Path, s: dict) -> list:
|
|
|
400
400
|
out.append(("warn", "released-brief", f"version {v.name}: "
|
|
401
401
|
f"released but still carries {brief} — a cut-local "
|
|
402
402
|
f"brief collapses at release"))
|
|
403
|
-
#
|
|
404
|
-
#
|
|
405
|
-
#
|
|
403
|
+
# One tier down the test inverts: a released plan is KEPT, so its
|
|
404
|
+
# presence is correct and what would be wrong is an unstamped one. An
|
|
405
|
+
# epic plan with no released banner means a release that did not go
|
|
406
|
+
# through `jarvis work release` — or a plan added to a cut afterwards,
|
|
407
|
+
# which is the same problem wearing different clothes.
|
|
408
|
+
from .epic import _RELEASED_MARKER
|
|
406
409
|
for e in v.epics:
|
|
407
|
-
if e.planned:
|
|
410
|
+
if e.planned and _RELEASED_MARKER not in e.md.read_text(errors="ignore"):
|
|
408
411
|
out.append(("warn", "released-brief", f"version {v.name}: "
|
|
409
|
-
f"released but epic '{e.name}'
|
|
410
|
-
f"
|
|
412
|
+
f"released but epic '{e.name}' carries a plan that is "
|
|
413
|
+
f"not stamped as released — it reads as live direction"))
|
|
411
414
|
return out
|
|
412
415
|
#: An agent-file reference in skill prose, in either of the two shapes an agent dir
|
|
413
416
|
#: takes: `.claude/agents/<name>.md` in a repo that keeps its own agents, and a bare
|
package/harness/harness/epic.py
CHANGED
|
@@ -220,13 +220,19 @@ _DURABLE_SECTIONS = ("## Governance this implies", "## Non-goals", "### Settled"
|
|
|
220
220
|
"### Forward-compat")
|
|
221
221
|
|
|
222
222
|
|
|
223
|
-
def
|
|
224
|
-
"""What
|
|
223
|
+
def plans_held(version) -> tuple:
|
|
224
|
+
"""What this cut's epic plans hold: `(plans, bytes, sections)`.
|
|
225
225
|
|
|
226
|
-
One derivation, because two surfaces quote
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
226
|
+
One derivation, because two surfaces quote it — the notice `release` prints as
|
|
227
|
+
it stamps the plans, and the line `status` shows a person deciding whether to
|
|
228
|
+
run it at all. Two surfaces that disagreed about what a cut carries would make
|
|
229
|
+
both worth less than either.
|
|
230
|
+
|
|
231
|
+
It measured what release was about to DELETE until 2026-09-12, when a released
|
|
232
|
+
plan started being kept instead. The number is the same number; what it is
|
|
233
|
+
evidence for is not. It now says how much settled prose is going quiet, which is
|
|
234
|
+
the argument for lifting the durable half into an owner file — not a last call
|
|
235
|
+
before the bytes go.
|
|
230
236
|
|
|
231
237
|
`sections` is every durable heading found across the plans, deduplicated: it is
|
|
232
238
|
what a reader needs to judge whether the text is safe to lose, and it is the
|
|
@@ -245,21 +251,23 @@ def removal_cost(version) -> tuple:
|
|
|
245
251
|
return len(plans), total, sorted(held)
|
|
246
252
|
|
|
247
253
|
|
|
248
|
-
def
|
|
249
|
-
"""What this release is about to
|
|
254
|
+
def _cost_of_closing(version, root) -> list:
|
|
255
|
+
"""What this release is about to stamp shut, as lines a person can act on.
|
|
256
|
+
|
|
257
|
+
**Said while the plans are still the live thing a reader would reach for.** The
|
|
258
|
+
text no longer leaves the tree — a released plan is stamped and kept — so this is
|
|
259
|
+
not a last call before the bytes go. It is the argument for the sweep: a stamped
|
|
260
|
+
plan is history, and a decision left in one is a decision the next session will
|
|
261
|
+
not find when it reads the owner file.
|
|
250
262
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
plans, 450,405 bytes, about a hundred durable statements with no owner file, and
|
|
256
|
-
the recovery commit had to be worked out by hand afterwards from `git log
|
|
257
|
-
--diff-filter=D`.
|
|
263
|
+
Measured 2026-09-11 on `01-one-board`: twelve plans, 450,405 bytes, about a
|
|
264
|
+
hundred durable statements with no owner file. Those plans were deleted by the
|
|
265
|
+
release that day and restored on 2026-09-12, which is why deletion is no longer
|
|
266
|
+
what this reports.
|
|
258
267
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
saying nothing.
|
|
268
|
+
It names the size and the sections that can outlive the plan. It does not judge
|
|
269
|
+
whether a statement is durable — that is the judgement a lint cannot make, and
|
|
270
|
+
claiming it would be worse than saying nothing.
|
|
263
271
|
"""
|
|
264
272
|
plans = [e for e in version.epics if e.planned]
|
|
265
273
|
if not plans:
|
|
@@ -275,57 +283,77 @@ def _cost_of_removing(version, root) -> list:
|
|
|
275
283
|
for h in _DURABLE_SECTIONS if h in text]
|
|
276
284
|
note = f" — holds §{', §'.join(held)}" if held else ""
|
|
277
285
|
out.append(f" {e.name} {len(text.encode()):,} bytes{note}")
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
f"
|
|
281
|
-
f"
|
|
286
|
+
return ([f"\n STAMPING {len(plans)} epic plan(s), {total:,} bytes, as released. "
|
|
287
|
+
f"They stay in the tree and every link to them keeps resolving — but a "
|
|
288
|
+
f"stamped plan is history, so anything durable belongs in the file that "
|
|
289
|
+
f"owns it:"]
|
|
282
290
|
+ out
|
|
283
|
-
+
|
|
284
|
-
|
|
285
|
-
"what cites a file that is gone"])
|
|
291
|
+
+ [" `jarvis work align` reports what cites a file that is gone"])
|
|
292
|
+
|
|
286
293
|
|
|
294
|
+
#: What a released plan opens with. Matched on the marker rather than the whole line,
|
|
295
|
+
#: so a cut reopened and released again finds its own stamp instead of stacking a second.
|
|
296
|
+
_RELEASED_MARKER = "> **A released plan"
|
|
287
297
|
|
|
288
|
-
def _still_holds(root) -> str:
|
|
289
|
-
"""The commit whose tree still contains these plans, or empty when git cannot say.
|
|
290
298
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
299
|
+
def _stamp_released(epic, version_name) -> bool:
|
|
300
|
+
"""Mark a plan as history in the file itself. True when it was added.
|
|
301
|
+
|
|
302
|
+
In the document rather than in the folder name, because a reader who followed a
|
|
303
|
+
link is holding the document and never saw the folder. After the frontmatter and
|
|
304
|
+
before the first heading, which is the one place it cannot be scrolled past.
|
|
296
305
|
"""
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
306
|
+
text = epic.md.read_text()
|
|
307
|
+
if _RELEASED_MARKER in text:
|
|
308
|
+
return False
|
|
309
|
+
banner = (f"{_RELEASED_MARKER} — `{version_name}` shipped "
|
|
310
|
+
f"{date.today().isoformat()}.**\n"
|
|
311
|
+
f"> It says how the work was planned, never what is true now. Anything here\n"
|
|
312
|
+
f"> that still governs belongs in the file that owns it — `jarvis work where "
|
|
313
|
+
f"<id>` finds one.\n\n")
|
|
314
|
+
if text.startswith("---\n"):
|
|
315
|
+
close = text.index("\n---\n", 3) + len("\n---\n")
|
|
316
|
+
text = text[:close] + "\n" + banner + text[close:].lstrip("\n")
|
|
317
|
+
else:
|
|
318
|
+
text = banner + text
|
|
319
|
+
epic.md.write_text(text)
|
|
320
|
+
return True
|
|
300
321
|
|
|
301
322
|
|
|
302
323
|
def cmd_epic_release(root, version) -> int:
|
|
303
|
-
"""
|
|
324
|
+
"""Stamp every `epic.md` in a released version as history, and KEEP it.
|
|
304
325
|
|
|
305
|
-
An epic is
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
326
|
+
An epic is temporary: how the work was planned stops being true the moment it
|
|
327
|
+
ships. What does not follow is that the file should go. Deleting it was the rule
|
|
328
|
+
until 2026-09-12, and the price was measured rather than argued: releasing
|
|
329
|
+
`01-one-board` removed twelve plans, left 104 links across the repo pointing at a
|
|
330
|
+
file that no longer existed, and put the text somewhere only a person who knew the
|
|
331
|
+
commit could reach. Nobody reads git for a decision.
|
|
309
332
|
|
|
310
|
-
|
|
311
|
-
the
|
|
312
|
-
|
|
313
|
-
|
|
333
|
+
So the plan stays where every link already points and travels to the archive with
|
|
334
|
+
the rest of the cut, which `links.repairing` handles like any other move. The
|
|
335
|
+
sweep that lifts durable statements into their owner files is still worth doing —
|
|
336
|
+
it is just no longer a race against an unlink.
|
|
337
|
+
|
|
338
|
+
Called from `cmd_release`, never on its own.
|
|
339
|
+
"""
|
|
340
|
+
for line in _cost_of_closing(version, root):
|
|
314
341
|
print(line)
|
|
315
|
-
|
|
342
|
+
stamped = []
|
|
316
343
|
for e in version.epics:
|
|
317
|
-
# An epic with no plan doc
|
|
318
|
-
#
|
|
319
|
-
#
|
|
320
|
-
# this exists to remove.
|
|
344
|
+
# An epic with no plan doc has nothing to stamp. It was also the case that
|
|
345
|
+
# unlinking regardless raised AFTER `released:` was stamped, leaving the cut
|
|
346
|
+
# reading as released with every other plan untouched.
|
|
321
347
|
if not e.planned:
|
|
322
348
|
continue
|
|
323
|
-
e.
|
|
324
|
-
|
|
325
|
-
if
|
|
326
|
-
print(f"
|
|
327
|
-
f"
|
|
328
|
-
return len(
|
|
349
|
+
if _stamp_released(e, version.name):
|
|
350
|
+
stamped.append(e.name)
|
|
351
|
+
if stamped:
|
|
352
|
+
print(f" stamped {len(stamped)} epic plan(s) as released and kept them "
|
|
353
|
+
f"in place: {', '.join(stamped)}")
|
|
354
|
+
return len(stamped)
|
|
355
|
+
|
|
356
|
+
|
|
329
357
|
def epic_for_task(root, args, version_name):
|
|
330
358
|
"""Resolve the `--epic` a task command names, and check it agrees with any
|
|
331
359
|
`--version` also given. Returns (epic, version_name) — the version is
|
package/harness/harness/shift.py
CHANGED
|
@@ -27,7 +27,7 @@ from .tree import BLOCKED, BUCKETS, die, find_work_root, rel
|
|
|
27
27
|
from .frontmatter import rewrite_file
|
|
28
28
|
from .model import locate, missing, record_session, scan
|
|
29
29
|
from .generate import _sync
|
|
30
|
-
from .epic import
|
|
30
|
+
from .epic import plans_held
|
|
31
31
|
from . import autonomy, events, links, peers
|
|
32
32
|
# The ceiling is read through the MODULE, never bound in with `from … import`.
|
|
33
33
|
# A `from .autonomy import CEILING` captures the value at import time, so
|
|
@@ -286,10 +286,11 @@ def cmd_status(args) -> int:
|
|
|
286
286
|
ready = [v for v in s["versions"] if v.finishable()]
|
|
287
287
|
print(f"WAITING ON YOU ({len(waiting) + len(ready)})")
|
|
288
288
|
for v in ready:
|
|
289
|
-
plans, size, held =
|
|
289
|
+
plans, size, held = plans_held(v)
|
|
290
290
|
cost = ""
|
|
291
291
|
if plans:
|
|
292
|
-
cost = (f" — releasing
|
|
292
|
+
cost = (f" — releasing stamps {plans} epic plan(s) as history, "
|
|
293
|
+
f"{size:,} bytes"
|
|
293
294
|
+ (f", holding §{', §'.join(held)}" if held else ""))
|
|
294
295
|
print(f" {v.name}: every task is complete and the cut is still open "
|
|
295
296
|
f"— `jarvis work release {v.name}`{cost}")
|
|
@@ -198,12 +198,12 @@ def cmd_release(args) -> int:
|
|
|
198
198
|
# how the work was planned stops being true once it ships.
|
|
199
199
|
cmd_epic_release(root, version)
|
|
200
200
|
print("\nDistill before archiving:")
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
# `git log --diff-filter=D`.
|
|
201
|
+
# The plans are still in the tree, stamped as history, so this points at them
|
|
202
|
+
# rather than at a commit. It sent a reader to files the same command had already
|
|
203
|
+
# removed until 2026-09-12, and the one time it mattered the recovery commit had
|
|
204
|
+
# to be reconstructed afterwards with `git log --diff-filter=D`.
|
|
205
205
|
print(" 1. Promote still-load-bearing decisions to the domain or system that owns "
|
|
206
|
-
"them —
|
|
206
|
+
"them — the plans are stamped and kept, so read them where they sit")
|
|
207
207
|
print(f" 2. Repoint any inbound deep-links to those {ids.LEDGER}-nn entries")
|
|
208
208
|
print(f" 3. {cli()} archive {name} (strips each task to task.md, and "
|
|
209
209
|
f"files a RELEASED cut under versions/complete/)")
|
|
@@ -308,9 +308,9 @@ files are deleted. **Read a dissolving `epic.md` in full first** — a thin epic
|
|
|
308
308
|
fully-planned, deliberately-deferred goal hides.
|
|
309
309
|
|
|
310
310
|
A version's status is derived: **planned** until tasks start, **current** while any is in-progress,
|
|
311
|
-
**released** once `jarvis work release` stamps it. Release **flattens the done tier
|
|
312
|
-
`epic.md
|
|
313
|
-
`work/archive/versions/<v>/`.
|
|
311
|
+
**released** once `jarvis work release` stamps it. Release **flattens the done tier** and **stamps
|
|
312
|
+
each `epic.md` as history, keeping it**, then `jarvis work archive` strips each task to its `task.md`
|
|
313
|
+
and moves the version to `work/archive/versions/<v>/`.
|
|
314
314
|
|
|
315
315
|
**Archived is off the board, not gone.** `list`, the README table and every lint see only `versions/`.
|
|
316
316
|
`jarvis work path <v>` and `jarvis work where <id>` still resolve into the archive.
|
|
@@ -338,8 +338,9 @@ is a brief and nothing else.
|
|
|
338
338
|
says so, and it is right: that is an epic wearing a task costume. Split it by *goal*, not by layer.
|
|
339
339
|
|
|
340
340
|
`epic.md` is mandatory for an epic and owns three things nothing else does — the **Goal** in user
|
|
341
|
-
terms, the **Plan** every task builds against, and **Governance this implies**.
|
|
342
|
-
`
|
|
341
|
+
terms, the **Plan** every task builds against, and **Governance this implies**. `jarvis work
|
|
342
|
+
release` **stamps it as history and keeps it** — it used to delete it, which cost 104 dangling links
|
|
343
|
+
and put the text where only somebody who knew the commit could read it (2026-09-12).
|
|
343
344
|
|
|
344
345
|
`handoff.md` is the **cross-conversation** artifact, not a per-task journal: **not** scaffolded on
|
|
345
346
|
pickup — create it with `jarvis work handoff <name>` only when handing the task to a fresh session. A task
|
|
@@ -459,8 +460,8 @@ The two task-count rules are one rule from both ends: **a task is one goal, an e
|
|
|
459
460
|
**done** epic is exempt from the floor; it is history, not a plan.
|
|
460
461
|
|
|
461
462
|
`jarvis work align` reports every misalignment class — dangling citation · orphan/uncovered AC · unenforced
|
|
462
|
-
rule · retired concept still named live · complete-but-unreleased version ·
|
|
463
|
-
|
|
463
|
+
rule · retired concept still named live · complete-but-unreleased version · a released cut whose
|
|
464
|
+
`epic.md` is not stamped as history. Report-only, exit 0.
|
|
464
465
|
|
|
465
466
|
## Every board write lands in git
|
|
466
467
|
|
|
@@ -716,8 +717,10 @@ The spend is authored prose. Keep it tight:
|
|
|
716
717
|
- **Cannot `new` or `epic-new` into a released version.**
|
|
717
718
|
- **Cannot `release`** a version with incomplete tasks, zero tasks, or no `outcome:`. **Cannot
|
|
718
719
|
`archive`** one that isn't released.
|
|
719
|
-
- **`release`
|
|
720
|
-
|
|
720
|
+
- **`release` stamps every `epic.md` as history and keeps it** (2026-09-12; it deleted them until
|
|
721
|
+
then). Nothing is lost and every link still resolves — but a stamped plan is history, so anything
|
|
722
|
+
durable in its §Plan or §Governance still belongs in the file that owns it. A reader who needs a
|
|
723
|
+
decision reads the owner file, and will not go looking in a shipped cut's plan.
|
|
721
724
|
- **Names are globally unique across tasks, epics and versions.** Renaming = move the folder by hand +
|
|
722
725
|
update any `depends_on:`.
|
|
723
726
|
- **`README.md` BACKLOG markers are load-bearing** — preserve them if you hand-edit around the table.
|
package/harness/test_work.py
CHANGED
|
@@ -506,21 +506,43 @@ def test_a_grandfathered_flat_version_still_reads():
|
|
|
506
506
|
assert [t.epic for t in ver.all_tasks()] == [None]
|
|
507
507
|
|
|
508
508
|
|
|
509
|
-
def
|
|
510
|
-
#
|
|
511
|
-
#
|
|
509
|
+
def test_release_stamps_the_plan_as_history_and_keeps_it():
|
|
510
|
+
# Release deleted the plan until 2026-09-12. It cost 104 dangling links across
|
|
511
|
+
# the repo and put the text where only somebody who knew the commit could read
|
|
512
|
+
# it, so the plan is now stamped and kept. What this guards is that the stamp
|
|
513
|
+
# actually lands in the file a reader opens — a folder-level marker would be
|
|
514
|
+
# invisible to anyone who arrived by following a link.
|
|
512
515
|
with tempfile.TemporaryDirectory() as tmp:
|
|
513
516
|
v = _tree(tmp)
|
|
514
517
|
e = _epic(v, "publish-under-your-name")
|
|
515
518
|
_task(e / "complete", "author-a-lesson")
|
|
516
519
|
version.cmd_epic_release(Path(tmp), model.Version(v))
|
|
517
|
-
assert
|
|
520
|
+
assert (e / "epic.md").is_file()
|
|
521
|
+
body = (e / "epic.md").read_text()
|
|
522
|
+
assert epic._RELEASED_MARKER in body
|
|
523
|
+
# Before the first heading, so it cannot be scrolled past.
|
|
524
|
+
assert body.index(epic._RELEASED_MARKER) < body.index("# ")
|
|
518
525
|
after = model.Version(v)
|
|
519
526
|
assert [x.name for x in after.epics] == ["publish-under-your-name"]
|
|
520
|
-
assert after.epics[0].planned is
|
|
527
|
+
assert after.epics[0].planned is True
|
|
521
528
|
assert [t.name for t in after.all_tasks()] == ["author-a-lesson"]
|
|
522
529
|
|
|
523
530
|
|
|
531
|
+
def test_releasing_twice_does_not_stack_a_second_stamp():
|
|
532
|
+
# A cut can be reopened and released again, and `align` reads the marker to tell
|
|
533
|
+
# a stamped plan from a live one. Two banners would still read as stamped, so
|
|
534
|
+
# this is about the document staying readable rather than about the check.
|
|
535
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
536
|
+
v = _tree(tmp)
|
|
537
|
+
e = _epic(v, "publish-under-your-name")
|
|
538
|
+
_task(e / "complete", "author-a-lesson")
|
|
539
|
+
version.cmd_epic_release(Path(tmp), model.Version(v))
|
|
540
|
+
first = (e / "epic.md").read_text()
|
|
541
|
+
version.cmd_epic_release(Path(tmp), model.Version(v))
|
|
542
|
+
assert (e / "epic.md").read_text() == first
|
|
543
|
+
assert first.count(epic._RELEASED_MARKER) == 1
|
|
544
|
+
|
|
545
|
+
|
|
524
546
|
def test_a_finished_epic_drops_into_the_done_tier_and_comes_back_out():
|
|
525
547
|
# The tier is DERIVED, so the only thing that may move an epic is what its
|
|
526
548
|
# tasks say — and it has to work in BOTH directions, or reopening a task would
|
|
@@ -951,13 +973,13 @@ def test_a_flat_version_is_grandfathered_but_a_mixed_one_is_not():
|
|
|
951
973
|
assert any("straggler: loose in version" in w for w in _warns(Path(tmp)))
|
|
952
974
|
|
|
953
975
|
|
|
954
|
-
def
|
|
976
|
+
def test_release_stamps_the_plan_of_an_epic_parked_in_the_done_tier():
|
|
955
977
|
# THE bug on release's only normal path. A version is releasable only when every
|
|
956
978
|
# task is complete — which is exactly the condition that parks each epic in
|
|
957
979
|
# `<v>/complete/`. `release` flattens that tier first, then removed `epic.md`
|
|
958
980
|
# from the PRE-MOVE model: FileNotFoundError, raised *after* `released:` was
|
|
959
|
-
# already stamped. A traceback, a half-applied release, and
|
|
960
|
-
#
|
|
981
|
+
# already stamped. A traceback, a half-applied release, and an unstamped plan
|
|
982
|
+
# left behind for `align` to report.
|
|
961
983
|
import os
|
|
962
984
|
with tempfile.TemporaryDirectory() as tmp:
|
|
963
985
|
root = Path(tmp)
|
|
@@ -970,10 +992,13 @@ def test_release_removes_epic_md_for_an_epic_parked_in_the_done_tier():
|
|
|
970
992
|
finally:
|
|
971
993
|
os.environ.pop("WORK_DIR")
|
|
972
994
|
|
|
973
|
-
# Flattened out of the done tier,
|
|
995
|
+
# Flattened out of the done tier, the plan kept and stamped, folder kept as
|
|
996
|
+
# the grouping.
|
|
974
997
|
assert (v / "a-shipped-goal").is_dir()
|
|
975
998
|
assert not (v / tree.DONE_TIER).exists()
|
|
976
|
-
|
|
999
|
+
plans = list(v.rglob("epic.md"))
|
|
1000
|
+
assert len(plans) == 1, "release must keep the plan, not remove it"
|
|
1001
|
+
assert epic._RELEASED_MARKER in plans[0].read_text()
|
|
977
1002
|
assert (v / "a-shipped-goal" / "complete" / "shipped-thing" / "task.md").is_file()
|
|
978
1003
|
assert model.Version(v).released
|
|
979
1004
|
|
|
@@ -6394,7 +6419,8 @@ def test_an_epic_whose_plan_doc_is_gone_still_moves_into_a_cut():
|
|
|
6394
6419
|
def test_releasing_a_cut_holding_an_epic_with_no_plan_doc_still_releases_it():
|
|
6395
6420
|
# The unlink ran on every epic, so one without a plan doc raised — and it raised
|
|
6396
6421
|
# AFTER `released:` was stamped, leaving the cut reading as released while every
|
|
6397
|
-
# other epic
|
|
6422
|
+
# other epic went untouched. The stamp has the same shape of hazard: an epic with
|
|
6423
|
+
# nothing to stamp must be skipped rather than read.
|
|
6398
6424
|
with tempfile.TemporaryDirectory() as tmp:
|
|
6399
6425
|
root = Path(tmp)
|
|
6400
6426
|
v = _tree(tmp)
|
|
@@ -6412,9 +6438,10 @@ def test_releasing_a_cut_holding_an_epic_with_no_plan_doc_still_releases_it():
|
|
|
6412
6438
|
|
|
6413
6439
|
after = model.locate_version(root, "26-cut")
|
|
6414
6440
|
assert after.released, "the cut never got its released stamp"
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6441
|
+
plans = list(after.folder.rglob("epic.md"))
|
|
6442
|
+
assert len(plans) == 1, "the epic that had a plan should still have exactly one"
|
|
6443
|
+
assert plans[0].parent.name == "publish-under-your-name"
|
|
6444
|
+
assert epic._RELEASED_MARKER in plans[0].read_text(), "the kept plan is unstamped"
|
|
6418
6445
|
|
|
6419
6446
|
|
|
6420
6447
|
def test_a_shipped_criterion_delivered_by_a_filed_cut_is_still_covered():
|
|
@@ -7403,13 +7430,13 @@ def test_the_stop_hook_being_switched_off_silences_the_standing_too():
|
|
|
7403
7430
|
|
|
7404
7431
|
|
|
7405
7432
|
|
|
7406
|
-
def
|
|
7407
|
-
# `release` has always printed "promote still-load-bearing decisions to the
|
|
7408
|
-
#
|
|
7409
|
-
# Measured on 01-one-board: twelve plans, 450,405 bytes,
|
|
7410
|
-
# statements with no owner file
|
|
7411
|
-
#
|
|
7412
|
-
#
|
|
7433
|
+
def test_a_release_says_what_it_is_stamping_shut():
|
|
7434
|
+
# `release` has always printed "promote still-load-bearing decisions to the domain
|
|
7435
|
+
# that owns them", and until 2026-09-12 it printed that AFTER unlinking the files
|
|
7436
|
+
# holding those decisions. Measured on 01-one-board: twelve plans, 450,405 bytes,
|
|
7437
|
+
# about a hundred durable statements with no owner file. The plans are now kept
|
|
7438
|
+
# and stamped rather than deleted, so what the notice owes a reader changed with
|
|
7439
|
+
# it: not a last call before the bytes go, but the argument for the sweep.
|
|
7413
7440
|
with tempfile.TemporaryDirectory() as tmp:
|
|
7414
7441
|
try:
|
|
7415
7442
|
repo = _git_repo(tmp, push=False)
|
|
@@ -7432,22 +7459,22 @@ def test_a_release_says_what_it_is_deleting_before_it_deletes_it():
|
|
|
7432
7459
|
with _work_dir(str(repo / "work")):
|
|
7433
7460
|
said = _capture_stdout(lambda: version.cmd_release({"name": "09-a-cut"}))
|
|
7434
7461
|
|
|
7435
|
-
#
|
|
7436
|
-
assert "
|
|
7462
|
+
# What is going quiet, in front of the person, in units they can weigh.
|
|
7463
|
+
assert "STAMPING 1 epic plan(s)" in said, said
|
|
7437
7464
|
assert "bytes" in said
|
|
7438
7465
|
# WHICH sections can outlive the plan — and not the ones that cannot.
|
|
7439
7466
|
assert "§Governance this implies" in said, said
|
|
7440
7467
|
assert "§Plan" not in said, "slice ordering is supposed to die with the epic"
|
|
7441
|
-
# Where it went, named at the moment it goes rather than reconstructed later.
|
|
7442
|
-
assert head and head in said, f"expected the recovery commit {head}: {said}"
|
|
7443
7468
|
|
|
7444
|
-
# And the claim is true: the plan is
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
assert "a rule is owed" in
|
|
7469
|
+
# And the claim is true: the plan is still there, and stamped.
|
|
7470
|
+
plan = v / "an-epic" / "epic.md"
|
|
7471
|
+
assert plan.is_file(), "a released plan is kept, not deleted"
|
|
7472
|
+
assert "a rule is owed" in plan.read_text()
|
|
7473
|
+
assert epic._RELEASED_MARKER in plan.read_text()
|
|
7448
7474
|
|
|
7449
|
-
#
|
|
7450
|
-
assert "no longer in the tree" in said, said
|
|
7475
|
+
# Nothing sends a reader to git for the text any more, because it is here.
|
|
7476
|
+
assert "no longer in the tree" not in said, said
|
|
7477
|
+
assert head not in said or "recoverable" not in said, said
|
|
7451
7478
|
finally:
|
|
7452
7479
|
events._PENDING.clear()
|
|
7453
7480
|
config.apply(config.DEFAULTS)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appchy/jarvis",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.114",
|
|
4
4
|
"description": "Jarvis — local AI coding assistant CLI",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -57,16 +57,16 @@
|
|
|
57
57
|
"typescript": "^5.7.0",
|
|
58
58
|
"vitest": "^2.1.0",
|
|
59
59
|
"@jarvis/agents": "1.0.0",
|
|
60
|
-
"@jarvis/anthropic": "1.0.0",
|
|
61
60
|
"@jarvis/board": "0.1.0",
|
|
62
|
-
"@jarvis/errors": "1.0.0",
|
|
63
61
|
"@jarvis/data": "0.1.0",
|
|
62
|
+
"@jarvis/errors": "1.0.0",
|
|
64
63
|
"@jarvis/logger": "1.0.0",
|
|
65
|
-
"@jarvis/types": "1.0.0",
|
|
66
64
|
"@jarvis/rpc": "1.0.0",
|
|
65
|
+
"@jarvis/anthropic": "1.0.0",
|
|
66
|
+
"@jarvis/types": "1.0.0",
|
|
67
67
|
"@jarvis/typescript-config": "1.0.0",
|
|
68
|
-
"@jarvis/
|
|
69
|
-
"@jarvis/
|
|
68
|
+
"@jarvis/vitest-config": "1.0.0",
|
|
69
|
+
"@jarvis/ui": "0.1.0"
|
|
70
70
|
},
|
|
71
71
|
"scripts": {
|
|
72
72
|
"dev": "tsx watch src/bin.ts start --foreground",
|