@worca/app 1.3.0 → 1.4.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 +85 -6
- package/agents/clarify.meta.json +1 -0
- package/agents/memoryDefragmenter.meta.json +2 -1
- package/agents/reviewer.meta.json +60 -0
- package/agents/worca-cc-code-reviewer.md +33 -0
- package/agents/worca-cc-memory-defragmenter.md +5 -3
- package/agents/workspaceScanner.meta.json +1 -0
- package/package.json +14 -10
- package/scripts/git-diff.mjs +25 -0
- package/scripts/gitDiff.meta.json +18 -0
- package/scripts/js-inline.mjs +11 -0
- package/scripts/js.meta.json +22 -0
- package/scripts/py-inline.py +27 -0
- package/scripts/py.meta.json +22 -0
- package/scripts/shell.meta.json +24 -0
- package/skills/worca/SKILL.md +3 -2
- package/src/cli/models.mjs +247 -0
- package/src/cli/render.mjs +72 -4
- package/src/cli/schedule.mjs +494 -0
- package/src/cli/worca-cc.mjs +1001 -22
- package/src/core/agent-registry.mjs +75 -23
- package/src/core/agent-store.mjs +51 -2
- package/src/core/artifacts.mjs +73 -10
- package/src/core/ask/events.mjs +119 -1
- package/src/core/ask/limits.mjs +32 -4
- package/src/core/ask/mcp-stdio.mjs +12 -0
- package/src/core/ask/model-deps.mjs +126 -0
- package/src/core/ask/model-proposal.mjs +370 -0
- package/src/core/ask/models.mjs +12 -0
- package/src/core/ask/policy-deps.mjs +124 -0
- package/src/core/ask/policy-proposal.mjs +363 -0
- package/src/core/ask/prompt.mjs +74 -9
- package/src/core/ask/proposal.mjs +54 -5
- package/src/core/ask/schedule-deps.mjs +83 -0
- package/src/core/ask/schedule-spec.mjs +310 -0
- package/src/core/ask/script-deps.mjs +357 -0
- package/src/core/ask/source-deps.mjs +52 -0
- package/src/core/ask/source-spec.mjs +157 -0
- package/src/core/ask/spawn.mjs +1 -0
- package/src/core/ask/store.mjs +6 -3
- package/src/core/ask/tool-deps.mjs +4 -0
- package/src/core/ask/tools.mjs +657 -37
- package/src/core/ask/turn.mjs +109 -2
- package/src/core/ask-files.mjs +406 -0
- package/src/core/ask-forms.mjs +195 -0
- package/src/core/ask-projection.mjs +72 -0
- package/src/core/bridge/errors.mjs +84 -0
- package/src/core/bridge/provider-ops.mjs +281 -0
- package/src/core/bridge/providers/copilot.mjs +269 -0
- package/src/core/bridge/providers/endpoint.mjs +257 -0
- package/src/core/bridge/registry.mjs +88 -0
- package/src/core/bridge/semaphore.mjs +73 -0
- package/src/core/bridge/server.mjs +184 -0
- package/src/core/bridge/telemetry.mjs +53 -0
- package/src/core/bridge/translate/request.mjs +252 -0
- package/src/core/bridge/translate/response.mjs +82 -0
- package/src/core/bridge/translate/stream.mjs +242 -0
- package/src/core/bridge/upstream.mjs +209 -0
- package/src/core/chat/command-router.mjs +58 -4
- package/src/core/chat/notifier.mjs +14 -1
- package/src/core/chat/renderers.mjs +35 -0
- package/src/core/claude-runner.mjs +126 -19
- package/src/core/config.mjs +212 -31
- package/src/core/cost-budget.mjs +3 -2
- package/src/core/db.mjs +169 -15
- package/src/core/failure-policy.mjs +10 -0
- package/src/core/fs-browse.mjs +16 -4
- package/src/core/git-info.mjs +22 -0
- package/src/core/graph/builtin-workflows.mjs +3 -1
- package/src/core/graph/exec-io.mjs +71 -0
- package/src/core/graph/executor.mjs +139 -70
- package/src/core/graph/human-evidence.mjs +131 -0
- package/src/core/graph/python-probe.mjs +172 -0
- package/src/core/graph/registry-ports.mjs +10 -6
- package/src/core/graph/scheduler.mjs +39 -24
- package/src/core/graph/script-child.mjs +81 -0
- package/src/core/graph/script-runner.mjs +597 -0
- package/src/core/graph/worca_script.py +207 -0
- package/src/core/guardrail-store.mjs +16 -0
- package/src/core/human-backfill.mjs +108 -0
- package/src/core/human-rate.mjs +17 -0
- package/src/core/index-html.mjs +6 -2
- package/src/core/memory-defrag-model.mjs +112 -0
- package/src/core/memory-store.mjs +70 -18
- package/src/core/memory-sync.mjs +22 -11
- package/src/core/metrics/read.mjs +4 -1
- package/src/core/metrics/record.mjs +50 -2
- package/src/core/metrics/sync.mjs +6 -4
- package/src/core/model-env.mjs +149 -0
- package/src/core/model-test.mjs +14 -1
- package/src/core/notifications.mjs +128 -0
- package/src/core/onboarding.mjs +8 -2
- package/src/core/orchestrator.mjs +357 -26
- package/src/core/phases.mjs +95 -6
- package/src/core/plugin-api.mjs +24 -7
- package/src/core/plugin-manifest.mjs +184 -18
- package/src/core/plugin-models.mjs +1 -0
- package/src/core/plugin-script-cases.mjs +118 -0
- package/src/core/plugin-store.mjs +163 -17
- package/src/core/plugin-workflows.mjs +71 -17
- package/src/core/policy/cache.mjs +116 -0
- package/src/core/policy/effective.mjs +175 -0
- package/src/core/policy/gate.mjs +91 -0
- package/src/core/policy/local.mjs +145 -0
- package/src/core/policy/registry.mjs +330 -0
- package/src/core/policy/scope.mjs +61 -0
- package/src/core/policy/state.mjs +79 -0
- package/src/core/policy/sync.mjs +513 -0
- package/src/core/protocol.mjs +43 -0
- package/src/core/run-harness.mjs +421 -72
- package/src/core/scheduler.mjs +980 -0
- package/src/core/script-bench.mjs +628 -0
- package/src/core/script-registry.mjs +116 -0
- package/src/core/script-store.mjs +563 -0
- package/src/core/settings.mjs +489 -14
- package/src/core/stats.mjs +33 -2
- package/src/core/workflow-export.mjs +94 -3
- package/src/core/workflow-share.mjs +67 -18
- package/src/core/workflows.mjs +47 -11
- package/src/core/workspaces.mjs +18 -12
- package/src/shared/forms/answer.mjs +164 -0
- package/src/shared/forms/catalog.mjs +91 -0
- package/src/shared/forms/form-def.mjs +290 -0
- package/src/shared/forms/layout.mjs +67 -0
- package/src/shared/forms/paths.mjs +47 -0
- package/src/shared/forms/project.mjs +309 -0
- package/src/shared/forms/schema.mjs +205 -0
- package/src/shared/graph/agent-meta.mjs +55 -5
- package/src/shared/graph/constants.mjs +14 -2
- package/src/shared/graph/flow-layout.mjs +2 -1
- package/src/shared/graph/isomorphic.mjs +5 -3
- package/src/shared/graph/manifest.mjs +22 -13
- package/src/shared/graph/ports.mjs +45 -19
- package/src/shared/graph/script-cases.mjs +257 -0
- package/src/shared/graph/script-icons.mjs +46 -0
- package/src/shared/graph/script-infer.mjs +259 -0
- package/src/shared/graph/script-meta.mjs +408 -0
- package/src/shared/graph/script-templates.mjs +201 -0
- package/src/shared/graph/template.mjs +4 -4
- package/src/shared/graph/validate.mjs +89 -16
- package/src/shared/human-estimate.mjs +100 -0
- package/src/shared/schedule/recurrence.mjs +353 -0
- package/src/shared/team-metrics/aggregate.mjs +51 -11
- package/{scripts → tools}/install.mjs +3 -3
- package/ui/public/app.js +4390 -683
- package/ui/public/artifact-picker.mjs +189 -0
- package/ui/public/ask/dom.mjs +121 -0
- package/ui/public/ask/form-preview.mjs +55 -0
- package/ui/public/ask/form-renderer.mjs +250 -0
- package/ui/public/ask/registry.mjs +53 -0
- package/ui/public/ask/widgets-display.mjs +370 -0
- package/ui/public/ask/widgets-input.mjs +624 -0
- package/ui/public/ask/widgets-layout.mjs +90 -0
- package/ui/public/ask-panel.mjs +401 -27
- package/ui/public/ask-run-card.mjs +1 -1
- package/ui/public/bridge-view.mjs +694 -0
- package/ui/public/chat-settings-view.mjs +24 -0
- package/ui/public/code-editor.mjs +181 -0
- package/ui/public/getting-started.mjs +34 -7
- package/ui/public/graph/composer.mjs +138 -10
- package/ui/public/graph/inspector.mjs +61 -58
- package/ui/public/graph/palette.mjs +27 -9
- package/ui/public/graph/run-decor.mjs +34 -17
- package/ui/public/graph/run-hosts.mjs +7 -1
- package/ui/public/graph/save-dialog.mjs +3 -0
- package/ui/public/graph/view.mjs +23 -7
- package/ui/public/guardrails-view.mjs +15 -3
- package/ui/public/guide-spot.mjs +87 -9
- package/ui/public/index.html +480 -141
- package/ui/public/memory-view.mjs +22 -4
- package/ui/public/models-view.mjs +162 -17
- package/ui/public/node-tunables.mjs +33 -4
- package/ui/public/plugins-view.mjs +23 -1
- package/ui/public/results-view.mjs +4 -2
- package/ui/public/schedule-sheet.mjs +430 -0
- package/ui/public/schedules-view.mjs +432 -0
- package/ui/public/script-bench-view.mjs +1154 -0
- package/ui/public/script-forms.mjs +282 -0
- package/ui/public/script-wizard.mjs +529 -0
- package/ui/public/scripts-view.mjs +868 -0
- package/ui/public/stats-view.mjs +159 -52
- package/ui/public/style.css +1461 -44
- package/ui/public/team-metrics-surfaces.mjs +77 -16
- package/ui/public/team-metrics-view.mjs +68 -5
- package/ui/public/team-policy-view.mjs +1402 -0
- package/ui/public/ui-level.mjs +237 -0
- package/ui/server.mjs +1827 -73
package/src/core/db.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { mkdirSync, existsSync } from 'node:fs';
|
|
|
21
21
|
import { join, dirname } from 'node:path';
|
|
22
22
|
import { worcaHome } from './projects.mjs';
|
|
23
23
|
import { maybeMigrateFromFs } from './migrate-fs-to-db.mjs';
|
|
24
|
+
import { backfillHumanHours } from './human-backfill.mjs';
|
|
24
25
|
import { SEED_TEMPLATES, NODE_ID_MAP, FB_WIRE_MAP } from './graph/seed-templates.mjs';
|
|
25
26
|
|
|
26
27
|
const _require = createRequire(import.meta.url);
|
|
@@ -54,7 +55,7 @@ const OPEN_BACKOFF_MS = 15;
|
|
|
54
55
|
/** Latest schema version. Bump + append a new migration step when the DDL grows.
|
|
55
56
|
* Exported so migration tests assert "reached the module's current version"
|
|
56
57
|
* instead of hardcoding the number — a schema bump then touches no test file. */
|
|
57
|
-
export const SCHEMA_VERSION =
|
|
58
|
+
export const SCHEMA_VERSION = 35;
|
|
58
59
|
|
|
59
60
|
/** Absolute path to the database file: <worcaHome>/worca-cc.db. */
|
|
60
61
|
export function dbPath() {
|
|
@@ -306,7 +307,9 @@ CREATE INDEX idx_pipeline_events_pipeline ON pipeline_events (pipeline_id, id);
|
|
|
306
307
|
CREATE TABLE clarify (
|
|
307
308
|
pipeline_id TEXT PRIMARY KEY,
|
|
308
309
|
questions TEXT, -- JSON: { questions: [ {id,question,options[2..4],allowFreeText} ] }
|
|
310
|
+
-- OR: the full resolved ask form { kind:'form', … } (spec §9)
|
|
309
311
|
answers TEXT, -- JSON: { answers: [ {id,question,choice} ] }
|
|
312
|
+
-- OR: { kind:'form', form, version, values } (spec §9)
|
|
310
313
|
FOREIGN KEY (pipeline_id) REFERENCES pipelines (id) ON DELETE CASCADE
|
|
311
314
|
);
|
|
312
315
|
|
|
@@ -521,7 +524,9 @@ CREATE TABLE IF NOT EXISTS step_questions (
|
|
|
521
524
|
node_id TEXT,
|
|
522
525
|
agent_key TEXT,
|
|
523
526
|
questions TEXT, -- JSON: { questions: [ {id,question,options[2..4],allowFreeText} ] }
|
|
527
|
+
-- OR: the full resolved ask form { kind:'form', … } (spec §9)
|
|
524
528
|
answers TEXT, -- JSON: { answers: [ {id,question,choice} ] }
|
|
529
|
+
-- OR: { kind:'form', form, version, values } (spec §9)
|
|
525
530
|
PRIMARY KEY (pipeline_id, step_key, round),
|
|
526
531
|
FOREIGN KEY (pipeline_id) REFERENCES pipelines (id) ON DELETE CASCADE
|
|
527
532
|
);
|
|
@@ -726,6 +731,86 @@ CREATE TABLE IF NOT EXISTS ask_card_comments (
|
|
|
726
731
|
);
|
|
727
732
|
`;
|
|
728
733
|
|
|
734
|
+
/** v31 (scheduled runs): launch tickets, recurring schedule parents, and the generic
|
|
735
|
+
* notification log. A run that waits for its start time lives HERE, never in
|
|
736
|
+
* `pipelines`: the pipeline row is still born inside run(), so no status-aware
|
|
737
|
+
* reader (stats, stale sweep, delete guard, team metrics) ever meets a waiting row.
|
|
738
|
+
* IF NOT EXISTS + INCREMENTAL_TABLES entries: reconcile-safe on a divergently
|
|
739
|
+
* stamped DB. An older build simply ignores all three tables. */
|
|
740
|
+
const SCHEDULED_RUNS_DDL = `
|
|
741
|
+
CREATE TABLE IF NOT EXISTS schedules (
|
|
742
|
+
id TEXT PRIMARY KEY, -- 'sch_' + 8 hex
|
|
743
|
+
title TEXT,
|
|
744
|
+
project_key TEXT,
|
|
745
|
+
project_dir TEXT,
|
|
746
|
+
workspace_id TEXT,
|
|
747
|
+
request TEXT NOT NULL, -- JSON: POST /api/run body shape (+ internal)
|
|
748
|
+
rule TEXT NOT NULL, -- JSON: recurrence rule (local time + tz)
|
|
749
|
+
overlap TEXT NOT NULL DEFAULT 'skip', -- skip | start | queue
|
|
750
|
+
max_failures INTEGER NOT NULL DEFAULT 3, -- 0 = never pause
|
|
751
|
+
failure_streak INTEGER NOT NULL DEFAULT 0,
|
|
752
|
+
if_missed TEXT NOT NULL DEFAULT 'run', -- run | skip
|
|
753
|
+
grace_min INTEGER NOT NULL DEFAULT 360,
|
|
754
|
+
status TEXT NOT NULL DEFAULT 'active', -- active | paused | ended
|
|
755
|
+
pause_reason TEXT, -- user | failure_streak
|
|
756
|
+
runs_count INTEGER NOT NULL DEFAULT 0,
|
|
757
|
+
next_run_at TEXT,
|
|
758
|
+
last_result TEXT, -- last occurrence outcome (display)
|
|
759
|
+
ask_thread_id TEXT, -- the Ask Worca card that made it (NULL = made by hand)
|
|
760
|
+
ask_card_id TEXT,
|
|
761
|
+
created_at TEXT NOT NULL,
|
|
762
|
+
updated_at TEXT NOT NULL
|
|
763
|
+
);
|
|
764
|
+
CREATE TABLE IF NOT EXISTS scheduled_runs (
|
|
765
|
+
id TEXT PRIMARY KEY, -- the runId UUID the fired run will carry
|
|
766
|
+
schedule_id TEXT REFERENCES schedules(id) ON DELETE CASCADE,
|
|
767
|
+
title TEXT,
|
|
768
|
+
project_key TEXT,
|
|
769
|
+
project_dir TEXT,
|
|
770
|
+
workspace_id TEXT,
|
|
771
|
+
run_at TEXT NOT NULL, -- UTC ISO instant
|
|
772
|
+
request TEXT NOT NULL, -- JSON: POST /api/run body shape (+ internal)
|
|
773
|
+
status TEXT NOT NULL DEFAULT 'scheduled', -- scheduled|firing|fired|canceled|skipped|missed|failed
|
|
774
|
+
if_missed TEXT NOT NULL DEFAULT 'run',
|
|
775
|
+
grace_min INTEGER NOT NULL DEFAULT 360,
|
|
776
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
777
|
+
retry_at TEXT, -- next attempt after a transient start error
|
|
778
|
+
queued INTEGER NOT NULL DEFAULT 0, -- overlap=queue: waiting for the previous run
|
|
779
|
+
forced INTEGER NOT NULL DEFAULT 0, -- Run now: start at once, bypass missed/overlap checks
|
|
780
|
+
owner_pid INTEGER, -- a waiting --wait CLI (or the firing process)
|
|
781
|
+
owner_host TEXT,
|
|
782
|
+
pipeline_id TEXT,
|
|
783
|
+
fail_reason TEXT,
|
|
784
|
+
ask_thread_id TEXT,
|
|
785
|
+
ask_card_id TEXT,
|
|
786
|
+
after_kind TEXT, -- v34: 'ticket' | 'pipeline' | NULL (a timed ticket)
|
|
787
|
+
after_id TEXT, -- v34: scheduled_runs.id | pipelines.id
|
|
788
|
+
after_policy TEXT NOT NULL DEFAULT 'done', -- v34: done | any
|
|
789
|
+
source_from_previous INTEGER NOT NULL DEFAULT 0, -- v34: start on the predecessor's feature branch
|
|
790
|
+
created_at TEXT NOT NULL,
|
|
791
|
+
updated_at TEXT NOT NULL
|
|
792
|
+
);
|
|
793
|
+
CREATE INDEX IF NOT EXISTS idx_scheduled_runs_due ON scheduled_runs (status, run_at);
|
|
794
|
+
CREATE INDEX IF NOT EXISTS idx_scheduled_runs_schedule ON scheduled_runs (schedule_id);
|
|
795
|
+
CREATE INDEX IF NOT EXISTS idx_scheduled_runs_after ON scheduled_runs (after_id);
|
|
796
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
797
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
798
|
+
scope TEXT NOT NULL DEFAULT 'schedule',
|
|
799
|
+
kind TEXT NOT NULL, -- missed|failed|skipped|late|paused|completed|run_error|run_paused|ended|retrying
|
|
800
|
+
severity TEXT NOT NULL DEFAULT 'problem', -- problem | info
|
|
801
|
+
schedule_id TEXT,
|
|
802
|
+
ticket_id TEXT,
|
|
803
|
+
pipeline_id TEXT,
|
|
804
|
+
project_dir TEXT,
|
|
805
|
+
title TEXT,
|
|
806
|
+
message TEXT NOT NULL,
|
|
807
|
+
created_at TEXT NOT NULL,
|
|
808
|
+
read_at TEXT,
|
|
809
|
+
resolved_at TEXT
|
|
810
|
+
);
|
|
811
|
+
CREATE INDEX IF NOT EXISTS idx_notifications_scope ON notifications (scope, created_at);
|
|
812
|
+
`;
|
|
813
|
+
|
|
729
814
|
const SCHEMA_V11 = `
|
|
730
815
|
ALTER TABLE config_workflow_nodes ADD COLUMN ask_questions INTEGER;
|
|
731
816
|
${STEP_QUESTIONS_DDL}
|
|
@@ -746,10 +831,14 @@ const INCREMENTAL_COLUMNS = {
|
|
|
746
831
|
source_type: "TEXT DEFAULT 'prompt'", source_ref: 'TEXT', guardrails_id: 'TEXT',
|
|
747
832
|
archived_at: 'TEXT', cost_cap_override: 'INTEGER NOT NULL DEFAULT 0',
|
|
748
833
|
pr_url: 'TEXT', pr_number: 'INTEGER', pr_state: 'TEXT', pr_checked_at: 'TEXT',
|
|
749
|
-
outcome: 'TEXT'
|
|
834
|
+
outcome: 'TEXT',
|
|
835
|
+
scheduled_for: 'TEXT', schedule_id: 'TEXT', // v31: scheduled-run provenance (NULL = started by hand)
|
|
836
|
+
policy_state: 'TEXT', // v32: team-policy run state (JSON: home, sha, overrides, exceeded, deviations, reason)
|
|
837
|
+
human_hours: 'REAL NOT NULL DEFAULT 0' }, // v33: Σ pipeline_steps.human_hours (money-saved design §6)
|
|
750
838
|
pipeline_steps: { session_id: 'TEXT', skills: 'TEXT', graphify_count: 'INTEGER',
|
|
751
839
|
execution_id: 'TEXT', exec_kind: 'TEXT', agent_key: 'TEXT', ended_at: 'TEXT',
|
|
752
|
-
exec_trigger: 'TEXT', exec_result: 'TEXT', exec_meta: 'TEXT'
|
|
840
|
+
exec_trigger: 'TEXT', exec_result: 'TEXT', exec_meta: 'TEXT',
|
|
841
|
+
human_hours: 'REAL', human_signals: 'TEXT' }, // v33: per-execution human-hours credit + its signals JSON
|
|
753
842
|
sub_agents: { ui_phase: 'TEXT', skills: 'TEXT', subagent_type: 'TEXT', graphify_count: 'INTEGER',
|
|
754
843
|
run_model: 'TEXT' }, // v25: the model the child actually ran on
|
|
755
844
|
workflows: { domain: 'TEXT', origin: 'TEXT', graph: 'TEXT', archived_at: 'TEXT' },
|
|
@@ -760,7 +849,11 @@ const INCREMENTAL_COLUMNS = {
|
|
|
760
849
|
mime: 'TEXT' }, // v27: sniffed mime; NULL on pre-v27 rows (= text)
|
|
761
850
|
project_config: { human_in_loop: 'INTEGER NOT NULL DEFAULT 1' }, // v28: the Auto entry's human-in-the-loop switch
|
|
762
851
|
diff_comments: { parent_id: 'TEXT REFERENCES diff_comments(id) ON DELETE CASCADE' }, // v29: reply threads; NULL = thread root
|
|
763
|
-
workspaces: { metrics_project: 'TEXT'
|
|
852
|
+
workspaces: { metrics_project: 'TEXT', // v30: team-metrics home (member absolute path); NULL = no home
|
|
853
|
+
policy_project: 'TEXT' }, // v32: team-policy home (member absolute path); NULL = no home
|
|
854
|
+
schedules: { ask_thread_id: 'TEXT', ask_card_id: 'TEXT' }, // v31: the Ask Worca card a series came from
|
|
855
|
+
scheduled_runs: { after_kind: 'TEXT', after_id: 'TEXT', after_policy: "TEXT NOT NULL DEFAULT 'done'",
|
|
856
|
+
source_from_previous: 'INTEGER NOT NULL DEFAULT 0' }, // v34: run chains
|
|
764
857
|
};
|
|
765
858
|
|
|
766
859
|
/** v23: per-loop-wire cycle budgets, the graph-engine twin of
|
|
@@ -801,6 +894,9 @@ const INCREMENTAL_TABLES = {
|
|
|
801
894
|
ask_worktrees: ASK_WORKTREES_DDL,
|
|
802
895
|
diff_comments: DIFF_COMMENTS_DDL,
|
|
803
896
|
ask_card_comments: DIFF_COMMENTS_DDL,
|
|
897
|
+
schedules: SCHEDULED_RUNS_DDL,
|
|
898
|
+
scheduled_runs: SCHEDULED_RUNS_DDL,
|
|
899
|
+
notifications: SCHEDULED_RUNS_DDL,
|
|
804
900
|
};
|
|
805
901
|
|
|
806
902
|
/**
|
|
@@ -816,6 +912,10 @@ const INCREMENTAL_INDEXES = {
|
|
|
816
912
|
table: 'ask_attachments',
|
|
817
913
|
ddl: 'CREATE INDEX IF NOT EXISTS idx_ask_attachments_thread ON ask_attachments (thread_id)',
|
|
818
914
|
},
|
|
915
|
+
idx_scheduled_runs_after: {
|
|
916
|
+
table: 'scheduled_runs',
|
|
917
|
+
ddl: 'CREATE INDEX IF NOT EXISTS idx_scheduled_runs_after ON scheduled_runs (after_id)',
|
|
918
|
+
},
|
|
819
919
|
};
|
|
820
920
|
|
|
821
921
|
/**
|
|
@@ -861,22 +961,29 @@ function schemaGaps(db) {
|
|
|
861
961
|
|
|
862
962
|
/** Apply the gap repairs with NO transaction control of its own — the caller owns
|
|
863
963
|
* the transaction (the ladder tx in migrate(), or reconcileSchema's own lock).
|
|
864
|
-
* ORDER IS LOAD-BEARING
|
|
865
|
-
*
|
|
866
|
-
*
|
|
867
|
-
*
|
|
868
|
-
*
|
|
869
|
-
*
|
|
870
|
-
*
|
|
871
|
-
*
|
|
964
|
+
* ORDER IS LOAD-BEARING, three ways:
|
|
965
|
+
* 1. Columns on the tables that ALREADY exist go first. A table's DDL block may carry a
|
|
966
|
+
* CREATE INDEX on an INCREMENTAL_COLUMNS column (v34: SCHEDULED_RUNS_DDL's
|
|
967
|
+
* idx_scheduled_runs_after names scheduled_runs.after_id), and INCREMENTAL_TABLES
|
|
968
|
+
* re-execs the WHOLE block when any sibling table (schedules / notifications) is
|
|
969
|
+
* missing — CREATE TABLE IF NOT EXISTS skips the old-shape table and the index
|
|
970
|
+
* would throw `no such column` inside getDb().
|
|
971
|
+
* 2. Then the tables; then the columns RE-probed against the post-CREATE schema.
|
|
972
|
+
* `gaps.columns` was computed BEFORE this pass ran, so it cannot see an incremental
|
|
973
|
+
* column on a table this pass is about to create (ask_run_links.comment_ids on a
|
|
974
|
+
* >=20-stamped DB missing the ask tables) — the ALTER would be skipped and the DB
|
|
975
|
+
* stamped current with the column absent, and only a LATER migrate() would heal it.
|
|
976
|
+
* 3. INCREMENTAL_INDEXES last, for the same reason as 1. */
|
|
872
977
|
function repairSchemaGaps(db, gaps) {
|
|
978
|
+
const addColumns = () => {
|
|
979
|
+
for (const { table, col, type } of missingColumns(db)) db.exec(`ALTER TABLE ${table} ADD COLUMN ${col} ${type}`);
|
|
980
|
+
};
|
|
981
|
+
addColumns();
|
|
873
982
|
// One DDL block can create several tables (ASK_DDL, DIFF_COMMENTS_DDL) — the
|
|
874
983
|
// Set collapses the duplicate keys to a single idempotent exec.
|
|
875
984
|
for (const ddl of new Set((gaps.tables || []).map((t) => INCREMENTAL_TABLES[t]))) db.exec(ddl);
|
|
985
|
+
addColumns();
|
|
876
986
|
for (const name of gaps.indexes || []) db.exec(INCREMENTAL_INDEXES[name].ddl);
|
|
877
|
-
for (const { table, col, type } of missingColumns(db)) {
|
|
878
|
-
db.exec(`ALTER TABLE ${table} ADD COLUMN ${col} ${type}`);
|
|
879
|
-
}
|
|
880
987
|
}
|
|
881
988
|
|
|
882
989
|
/**
|
|
@@ -1157,6 +1264,48 @@ function applySchemaV30(db) {
|
|
|
1157
1264
|
repairSchemaGaps(db, schemaGaps(db));
|
|
1158
1265
|
}
|
|
1159
1266
|
|
|
1267
|
+
/** v31 (scheduled runs): schedules + scheduled_runs + notifications (INCREMENTAL_TABLES)
|
|
1268
|
+
* and pipelines.scheduled_for/schedule_id (INCREMENTAL_COLUMNS) — applySchemaV30's shape. */
|
|
1269
|
+
function applySchemaV31(db) {
|
|
1270
|
+
repairSchemaGaps(db, schemaGaps(db));
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/** v32 (team policy): pipelines.policy_state + workspaces.policy_project — two plain
|
|
1274
|
+
* additive columns declared in INCREMENTAL_COLUMNS, applySchemaV30's shape. NULL on
|
|
1275
|
+
* every existing row = no team policy touched this run / no policy home yet.
|
|
1276
|
+
* It is v32, not v31: scheduled runs claimed 31 on dev first, and a DB already
|
|
1277
|
+
* stamped 31 by that ladder would skip these columns forever (the v11 collision
|
|
1278
|
+
* this file's INCREMENTAL_COLUMNS comment describes). */
|
|
1279
|
+
function applySchemaV32(db) {
|
|
1280
|
+
repairSchemaGaps(db, schemaGaps(db));
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/** v33 (money saved): pipelines.human_hours + pipeline_steps.human_hours/human_signals —
|
|
1284
|
+
* additive columns declared in INCREMENTAL_COLUMNS, applySchemaV32's shape. Task 5 adds
|
|
1285
|
+
* the run-level backfill call for pre-v33 runs here. */
|
|
1286
|
+
function applySchemaV33(db) {
|
|
1287
|
+
repairSchemaGaps(db, schemaGaps(db));
|
|
1288
|
+
// Legacy credit for every terminal run recorded before v33. Best-effort by construction;
|
|
1289
|
+
// a throw here would roll the ladder back, so it is fenced. Runs once (the fast path
|
|
1290
|
+
// reconcileSchema never calls it). Cost: one read per indexed plan/review markdown.
|
|
1291
|
+
try { backfillHumanHours(db); } catch { /* never fail the migration on the backfill */ }
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/** v34 (run chains): scheduled_runs.after_kind/after_id/after_policy/source_from_previous + the
|
|
1295
|
+
* after_id index — INCREMENTAL_COLUMNS / INCREMENTAL_INDEXES, applySchemaV30's shape. */
|
|
1296
|
+
function applySchemaV34(db) {
|
|
1297
|
+
repairSchemaGaps(db, schemaGaps(db));
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/** v35 (Opus 5.5 replaces Opus 5 in PREDEFINED_MODELS): V26's catalog swap again —
|
|
1301
|
+
* same reasons, same stores, same rules (renameStoredModelPins). History keeps
|
|
1302
|
+
* recording `claude-opus-5` where that is what ran. */
|
|
1303
|
+
const V35_MODEL_RENAMES = [['claude-opus-5', 'claude-opus-5-5']];
|
|
1304
|
+
|
|
1305
|
+
function applySchemaV35(db) {
|
|
1306
|
+
for (const [from, to] of V35_MODEL_RENAMES) renameStoredModelPins(db, from, to);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1160
1309
|
/** Move every stored pin on model id `from` (lower-case) to `to`. Each table
|
|
1161
1310
|
* is guarded like V24's: hand-seeded upgrade fixtures (and a DB from before the
|
|
1162
1311
|
* fs->db import) reach this step without some of them. */
|
|
@@ -1543,6 +1692,11 @@ export function migrate(db) {
|
|
|
1543
1692
|
if (current < 28) applySchemaV28(db); // Auto workflow: human_in_loop + flip to wf_auto
|
|
1544
1693
|
if (current < 29) applySchemaV29(db); // diff-comment reply threads: parent_id
|
|
1545
1694
|
if (current < 30) applySchemaV30(db); // team metrics: workspaces.metrics_project
|
|
1695
|
+
if (current < 31) applySchemaV31(db); // scheduled runs: tickets + schedules + notifications
|
|
1696
|
+
if (current < 32) applySchemaV32(db); // team policy: pipelines.policy_state + workspaces.policy_project
|
|
1697
|
+
if (current < 33) applySchemaV33(db); // money saved: human_hours columns
|
|
1698
|
+
if (current < 34) applySchemaV34(db); // run chains: scheduled_runs.after_* + source_from_previous
|
|
1699
|
+
if (current < 35) applySchemaV35(db); // Opus 5 pins -> Opus 5.5 (catalog swap)
|
|
1546
1700
|
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
1547
1701
|
db.exec('COMMIT');
|
|
1548
1702
|
} catch (err) {
|
|
@@ -40,6 +40,10 @@ export const REASON = Object.freeze({
|
|
|
40
40
|
ERROR: 'error', // a failure that would otherwise have ended the run
|
|
41
41
|
COST_PIPELINE: 'cost_pipeline', // the per-pipeline cost cap
|
|
42
42
|
COST_TOTAL: 'cost_total', // the total (weekly/monthly) cost cap
|
|
43
|
+
// Team policy (soft caps set on the worca-policy branch): the same two gates, but the
|
|
44
|
+
// binding number came from the team, so the resume flow offers "continue past".
|
|
45
|
+
COST_PIPELINE_POLICY: 'cost_pipeline_policy',
|
|
46
|
+
COST_TOTAL_POLICY: 'cost_total_policy',
|
|
43
47
|
});
|
|
44
48
|
export const REASON_CODES = Object.freeze(Object.values(REASON));
|
|
45
49
|
|
|
@@ -92,6 +96,10 @@ export const FAILURE_POLICY = Object.freeze({
|
|
|
92
96
|
budget: Object.freeze({
|
|
93
97
|
cost_pipeline: both(pause(REASON.COST_PIPELINE)),
|
|
94
98
|
cost_total: both(pause(REASON.COST_TOTAL)),
|
|
99
|
+
// Team soft caps pause only when someone can click "continue past": the harness
|
|
100
|
+
// downgrades them to a warning under --yes before ever reaching this row.
|
|
101
|
+
cost_pipeline_policy: both(pause(REASON.COST_PIPELINE_POLICY)),
|
|
102
|
+
cost_total_policy: both(pause(REASON.COST_TOTAL_POLICY)),
|
|
95
103
|
}),
|
|
96
104
|
// run()'s setup — checkout, graph build, skills gate — failed with the pipeline
|
|
97
105
|
// row already created. A pause here stamps `setupIncomplete`; resume replays it.
|
|
@@ -177,6 +185,8 @@ const CONSEQUENCES = Object.freeze({
|
|
|
177
185
|
[REASON.RECOVERABLE]: { reportsToSource: true, stagesResults: false, severity: 'warning', notifyPref: 'paused', exitInteractive: 0, label: 'recoverable error — resume to retry' },
|
|
178
186
|
[REASON.COST_PIPELINE]:{ reportsToSource: true, stagesResults: false, severity: 'warning', notifyPref: 'paused', exitInteractive: 0, label: 'pipeline cost limit reached' },
|
|
179
187
|
[REASON.COST_TOTAL]: { reportsToSource: true, stagesResults: false, severity: 'warning', notifyPref: 'paused', exitInteractive: 0, label: 'total cost limit reached' },
|
|
188
|
+
[REASON.COST_PIPELINE_POLICY]: { reportsToSource: true, stagesResults: false, severity: 'warning', notifyPref: 'paused', exitInteractive: 0, label: 'team cost cap reached' },
|
|
189
|
+
[REASON.COST_TOTAL_POLICY]: { reportsToSource: true, stagesResults: false, severity: 'warning', notifyPref: 'paused', exitInteractive: 0, label: 'team total cap reached' },
|
|
180
190
|
[REASON.ERROR]: { reportsToSource: true, stagesResults: true, severity: 'error', notifyPref: 'error', exitInteractive: 1, label: 'a step failed' },
|
|
181
191
|
});
|
|
182
192
|
|
package/src/core/fs-browse.mjs
CHANGED
|
@@ -8,14 +8,26 @@
|
|
|
8
8
|
import { readdir, stat } from 'node:fs/promises';
|
|
9
9
|
import { dirname, join, resolve } from 'node:path';
|
|
10
10
|
import { normalizeProjectPath } from './projects.mjs';
|
|
11
|
-
import { defaultRoot } from './settings.mjs';
|
|
11
|
+
import { defaultRoot, getProjectsRoot } from './settings.mjs';
|
|
12
12
|
|
|
13
13
|
function err(message, code) { return Object.assign(new Error(message), { code }); }
|
|
14
14
|
|
|
15
|
+
/** Where a blank listing opens: the projects root if it is a directory, else home. */
|
|
16
|
+
async function startFolder(home) {
|
|
17
|
+
const root = getProjectsRoot();
|
|
18
|
+
try {
|
|
19
|
+
if (root !== home && (await stat(root)).isDirectory()) return root;
|
|
20
|
+
} catch { /* missing or unreadable root: fall back to home */ }
|
|
21
|
+
return home;
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
/**
|
|
16
25
|
* List the sub-directories of `input` (tilde-expanded, resolved). Empty input
|
|
17
|
-
* lists the
|
|
18
|
-
*
|
|
26
|
+
* lists the effective projects root when it is an existing directory, else the
|
|
27
|
+
* OS home directory (normalizeProjectPath returns null for blank input, so this
|
|
28
|
+
* can never fall through to process.cwd()). With nothing configured the two are
|
|
29
|
+
* the same folder; in a container, WORCA_PROJECTS_ROOT names the mounted repos
|
|
30
|
+
* while home is an empty /home/worca. `home` stays the OS home (the Home button).
|
|
19
31
|
* @param {string} input
|
|
20
32
|
* @returns {Promise<{path:string, parent:string|null, home:string,
|
|
21
33
|
* dirs:Array<{name:string, path:string}>}>} parent is null at the fs root.
|
|
@@ -24,7 +36,7 @@ function err(message, code) { return Object.assign(new Error(message), { code })
|
|
|
24
36
|
*/
|
|
25
37
|
export async function listFolders(input) {
|
|
26
38
|
const home = resolve(defaultRoot());
|
|
27
|
-
const path = normalizeProjectPath(input) || home;
|
|
39
|
+
const path = normalizeProjectPath(input) || await startFolder(home);
|
|
28
40
|
let entries;
|
|
29
41
|
try {
|
|
30
42
|
entries = await readdir(path, { withFileTypes: true });
|
package/src/core/git-info.mjs
CHANGED
|
@@ -438,6 +438,28 @@ export async function listRemotes(projectDir) {
|
|
|
438
438
|
return { ok: true, remotes };
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
+
/**
|
|
442
|
+
* The branches each named remote has, from the LOCAL remote-tracking refs
|
|
443
|
+
* (`refs/remotes/<remote>/*` — no network, as fresh as the last fetch). A remote
|
|
444
|
+
* name may itself hold a slash, so the longest matching name owns a ref; the
|
|
445
|
+
* symbolic `HEAD` is dropped. Never throws:
|
|
446
|
+
* { ok:true, byRemote:{ [name]: string[] } } | { ok:false, byRemote:{}, error }.
|
|
447
|
+
*/
|
|
448
|
+
export async function listRemoteBranches(projectDir, remoteNames = []) {
|
|
449
|
+
if (!projectDir) return { ok: false, byRemote: {}, error: 'projectDir is required' };
|
|
450
|
+
const r = await _run('git', ['for-each-ref', '--format=%(refname)', 'refs/remotes/'], { cwd: projectDir });
|
|
451
|
+
if (!r.ok) return { ok: false, byRemote: {}, error: (r.stderr || '').trim() || `git exited ${r.code}` };
|
|
452
|
+
const names = [...remoteNames].sort((a, b) => b.length - a.length);
|
|
453
|
+
const byRemote = Object.fromEntries(remoteNames.map((n) => [n, []]));
|
|
454
|
+
for (const raw of (r.stdout || '').split(/\r?\n/)) {
|
|
455
|
+
const rest = raw.trim().startsWith('refs/remotes/') ? raw.trim().slice('refs/remotes/'.length) : '';
|
|
456
|
+
const name = rest && names.find((n) => rest.startsWith(`${n}/`));
|
|
457
|
+
const branch = name ? rest.slice(name.length + 1) : '';
|
|
458
|
+
if (branch && branch !== 'HEAD') byRemote[name].push(branch);
|
|
459
|
+
}
|
|
460
|
+
return { ok: true, byRemote };
|
|
461
|
+
}
|
|
462
|
+
|
|
441
463
|
// Test seam: swap the command runner + clear the gh memo. Mirrors server.mjs#_testing.
|
|
442
464
|
export const _testing = {
|
|
443
465
|
defaultRun,
|
|
@@ -83,7 +83,9 @@ export const GRAPH_MEMORY_DEFRAG_WORKFLOW = deepFreeze({
|
|
|
83
83
|
updatedAt: '1970-01-01T00:00:00.000Z',
|
|
84
84
|
nodes: [
|
|
85
85
|
{ id: 'n_task', kind: 'task', x: 40, y: 200, config: {} },
|
|
86
|
-
|
|
86
|
+
// Sonnet 5 by default: a bounded restructure of a few markdown files. The template layer
|
|
87
|
+
// is the lowest (workflows.mjs resolveGraph), so a per-role or per-node pick still wins.
|
|
88
|
+
{ id: 'n_defrag', kind: 'agent', key: 'memoryDefragmenter', x: 320, y: 200, config: { model: 'claude-sonnet-5' } },
|
|
87
89
|
{ id: 'n_end', kind: 'end', x: 600, y: 200, config: {} },
|
|
88
90
|
],
|
|
89
91
|
wires: [
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/core/graph/exec-io.mjs
|
|
2
|
+
//
|
|
3
|
+
// Execution I/O shared by every executor that publishes ports and reads a verdict:
|
|
4
|
+
// the agent executor (executor.mjs) and the script runner (script-runner.mjs). Moved
|
|
5
|
+
// out of executor.mjs so the script runner needs no executor import and no cycle;
|
|
6
|
+
// executor.mjs re-exports `readVerdict` and `publishable` for its existing importers.
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { relative, basename } from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { normalizeReview, safeParseJson } from '../protocol.mjs';
|
|
12
|
+
|
|
13
|
+
// ── verdicts ──────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/** The text every unparseable verdict file fails with. */
|
|
16
|
+
const BAD_VERDICT_TAIL = 'expected { "issues": [ \u2026 ] }';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read a node's verdict JSON back through the protocol normalizer.
|
|
20
|
+
*
|
|
21
|
+
* Two degenerate cases, deliberately split (they are NOT the same failure):
|
|
22
|
+
* - the file was NEVER WRITTEN -> `{issues: [], summary: '', missing: true}`: a clean
|
|
23
|
+
* pass, v1 parity, because an agent that declares a verdict and writes none must not
|
|
24
|
+
* fail a run. `missing` is the flag the caller turns into a warning (and the reason
|
|
25
|
+
* the reviews table skips the row) instead of a phantom zero-issue review.
|
|
26
|
+
* - the file EXISTS but does not parse, or carries no `issues` array -> THROW. The
|
|
27
|
+
* verifier wrote garbage, and on every shipped seed the clean side is wired straight
|
|
28
|
+
* to End, so "no issues" there is indistinguishable from an approval. Fail-fast owns
|
|
29
|
+
* the rest.
|
|
30
|
+
*
|
|
31
|
+
* `readReview` is untouched for its other callers (it is v1 code with its own tolerant
|
|
32
|
+
* contract); the existsSync + parse-failure branch lives here.
|
|
33
|
+
*/
|
|
34
|
+
export async function readVerdict(verdictPath) {
|
|
35
|
+
if (!verdictPath) return { issues: [], summary: '' };
|
|
36
|
+
if (!existsSync(verdictPath)) return { issues: [], summary: '', missing: true };
|
|
37
|
+
let text;
|
|
38
|
+
try {
|
|
39
|
+
text = await readFile(verdictPath, 'utf8');
|
|
40
|
+
} catch (err) {
|
|
41
|
+
throw Object.assign(new Error(`verdict file unreadable: ${verdictPath} — ${err?.message || err}`),
|
|
42
|
+
{ code: 'BAD_VERDICT' });
|
|
43
|
+
}
|
|
44
|
+
const data = safeParseJson(text);
|
|
45
|
+
if (!data || typeof data !== 'object' || !Array.isArray(data.issues)) {
|
|
46
|
+
throw Object.assign(new Error(`verdict file is not a review JSON: ${verdictPath} — ${BAD_VERDICT_TAIL}`),
|
|
47
|
+
{ code: 'BAD_VERDICT' });
|
|
48
|
+
}
|
|
49
|
+
return normalizeReview(data);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The warning line a missing verdict raises, relative to the pipeline dir so the
|
|
53
|
+
* run log stays readable. */
|
|
54
|
+
export function missingVerdictWarning(ctx, verdictPath) {
|
|
55
|
+
const rel = ctx?.pipelineDir ? relative(ctx.pipelineDir, verdictPath) : basename(verdictPath);
|
|
56
|
+
return `verdict file missing: ${ctx?.nodeId || ctx?.node?.id || '?'} ${rel} — treated as clean`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── publishing ────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/** The output map the scheduler publishes from: an entry per declared port, with a
|
|
62
|
+
* path where one was allocated and an empty payload for void ports. Exported for
|
|
63
|
+
* P4's composite `finish` arm. */
|
|
64
|
+
export function publishable(ports, outputs) {
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const port of ports?.outputs || []) {
|
|
67
|
+
if (!port) continue;
|
|
68
|
+
out[port.id] = outputs[port.id]?.path ? { path: outputs[port.id].path } : {};
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|